diff --git a/assets/dashboard/index.html b/assets/dashboard/index.html index 6122f3f..400c750 100644 --- a/assets/dashboard/index.html +++ b/assets/dashboard/index.html @@ -408,7 +408,9 @@

${t().explain2Title}

// 대시보드는 GitHub Pages(spot.roboco.io), 데이터 피드는 CloudFront — 절대 URL로 교차 출처 fetch (피드는 CORS 전면 허용) const FEED_URL = 'https://d16jdvzof4zpo7.cloudfront.net/data.json'; -DATA = await (await fetch(FEED_URL + '?t=' + Math.floor(Date.now()/60000), {cache:'no-store'})).json(); +// cache:'no-store' 금지 — 요청에 Cache-Control: no-cache가 붙으면 CloudFront 응답에서 CORS 헤더가 빠진다. +// 신선도는 분 단위 ?t= 쿼리로 확보한다. +DATA = await (await fetch(FEED_URL + '?t=' + Math.floor(Date.now()/60000))).json(); renderChrome(); // fill in DATA-dependent sub/legend now that DATA is available // ---- 확보 안정성 순위·타일 (데이터에서 자동 계산) ---- diff --git a/lib/spot-score-collector-stack.ts b/lib/spot-score-collector-stack.ts index 131073e..eb9bb5c 100644 --- a/lib/spot-score-collector-stack.ts +++ b/lib/spot-score-collector-stack.ts @@ -161,6 +161,17 @@ function handler(event) { return event.request; }`), }); + // 관리형 CORS-AllowAllOrigins는 요청에 Cache-Control: no-cache가 있으면 CORS 헤더가 빠지는 현상이 + // 관측되어(2026-10-12, 브라우저 fetch cache:'no-store') 오리진 응답과 무관하게 항상 덮어쓰는 커스텀 정책 사용 + const feedCorsPolicy = new cloudfront.ResponseHeadersPolicy(this, 'FeedCorsPolicy', { + corsBehavior: { + accessControlAllowOrigins: ['*'], + accessControlAllowMethods: ['GET', 'HEAD', 'OPTIONS'], + accessControlAllowHeaders: ['*'], + accessControlAllowCredentials: false, + originOverride: true, + }, + }); const dashCdn = new cloudfront.Distribution(this, 'DashboardCdn', { defaultBehavior: { origin: new origins.HttpOrigin(dashBucket.bucketWebsiteDomainName, { @@ -168,8 +179,8 @@ function handler(event) { }), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: dashCachePolicy, - // data.json을 타 오리진에서 fetch로 쓸 수 있게 CORS 허용 (오픈 데이터 피드) - responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS, + // data.json을 타 오리진(spot.roboco.io 대시보드 등)에서 fetch로 쓸 수 있게 CORS 허용 (오픈 데이터 피드) + responseHeadersPolicy: feedCorsPolicy, functionAssociations: [ { function: dashRedirectFn, eventType: cloudfront.FunctionEventType.VIEWER_REQUEST }, ], diff --git a/test/spot-score-collector-stack.test.ts b/test/spot-score-collector-stack.test.ts index 37ec630..1569f99 100644 --- a/test/spot-score-collector-stack.test.ts +++ b/test/spot-score-collector-stack.test.ts @@ -89,8 +89,7 @@ test('CloudFront가 S3 website 오리진 앞에서 HTTPS 리다이렉트 + CORS DistributionConfig: Match.objectLike({ DefaultCacheBehavior: Match.objectLike({ ViewerProtocolPolicy: 'redirect-to-https', - // 관리형 정책 CORS-AllowAllOrigins (data.json 오픈 데이터 피드용) - ResponseHeadersPolicyId: '60669652-455b-4ae9-85a4-c4c02393f86c', + ResponseHeadersPolicyId: Match.anyValue(), }), Origins: Match.arrayWith([ Match.objectLike({ @@ -117,6 +116,18 @@ test('기존 CloudFront 대시보드 경로(/, /index.html)는 spot.roboco.io로 }); }); +test('피드 CORS 정책은 모든 출처 허용 + 오리진 응답 덮어쓰기', () => { + const t = makeCollectorTemplate(); + t.hasResourceProperties('AWS::CloudFront::ResponseHeadersPolicy', { + ResponseHeadersPolicyConfig: Match.objectLike({ + CorsConfig: Match.objectLike({ + AccessControlAllowOrigins: { Items: ['*'] }, + OriginOverride: true, + }), + }), + }); +}); + test('역할에 GetSpotPlacementScores와 테이블 BatchWriteItem 권한', () => { const t = makeCollectorTemplate(); // arrayWith는 패턴 순서를 부분수열로 요구하므로 문장별로 따로 단언한다