From 2d1d7ef78be756e8948baa896cd6711c1bb052a0 Mon Sep 17 00:00:00 2001 From: Jung Do Hyun Date: Mon, 12 Oct 2026 23:26:24 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=20CORS=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=A0=95=EC=B1=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=90=EC=B2=B4=20=E2=80=94=20no-cache=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=20=EC=8B=9C=20CORS=20=ED=97=A4=EB=8D=94=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spot.roboco.io 대시보드에서 data.json fetch가 CORS로 차단됐다. 관리형 CORS-AllowAllOrigins 정책은 요청에 Cache-Control: no-cache가 있으면 access-control-allow-origin을 붙이지 않았고, fetch의 cache:'no-store'가 이 헤더를 보낸다. - 피드 CloudFront: 커스텀 ResponseHeadersPolicy(origin *, originOverride) - index.html: cache:'no-store' 제거 (신선도는 분 단위 ?t= 쿼리로 확보) Co-Authored-By: Claude Opus 5.5 --- assets/dashboard/index.html | 4 +++- lib/spot-score-collector-stack.ts | 15 +++++++++++++-- test/spot-score-collector-stack.test.ts | 15 +++++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) 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는 패턴 순서를 부분수열로 요구하므로 문장별로 따로 단언한다