Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion assets/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,9 @@ <h3>${t().explain2Title}</h3>

// 대시보드는 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

// ---- 확보 안정성 순위·타일 (데이터에서 자동 계산) ----
Expand Down
15 changes: 13 additions & 2 deletions lib/spot-score-collector-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,26 @@ 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, {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
}),
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 },
],
Expand Down
15 changes: 13 additions & 2 deletions test/spot-score-collector-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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는 패턴 순서를 부분수열로 요구하므로 문장별로 따로 단언한다
Expand Down
Loading