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는 패턴 순서를 부분수열로 요구하므로 문장별로 따로 단언한다