도메인별 스키마(company·place·fact·local·site·job)를 걷어내고 public 한 벌로 폈다.
스키마 한정자가 붙은 순간부터 ORM·raw SQL·테스트 픽스처가 각자 그 이름을 들고 다녀야 했다.
- 공용 콘텐츠를 한 테이블로 되돌린다. spots·region_stories 를 따로 파 놓고 보니
같은 성격이 세 곳으로 갈라져 있었다 — `area_contents` 가 처음부터 content_type 으로
종류를 가르는 설계였고 그걸 쓰면 됐다. 관계(거리·숨김)만 `place_area_refs` 로 남긴다.
- migrations/ + scripts/migrate.py: `init.sql` 은 **DB 를 처음 만들 때만** 돈다. 파일에
컬럼을 더해도 이미 데이터가 든 DB 에는 반영되지 않는다 — 실제로 TourAPI 가 주변 정보를
받아 와도 저장할 곳이 없어 축제·맛집이 0건이었고, 화면에는 "그냥 안 나오는 것" 으로만 보였다.
DECISIONS.md 가 예고한 그대로다("운영 DB 가 생기는 순간 다시 필요해진다").
Alembic 을 쓰지 않는 이유는 스키마 정의가 이미 두 곳(ORM·init.sql)이라 세 번째를
더하면 어긋날 자리가 하나 더 생기기 때문이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
42 lines
1.8 KiB
TypeScript
42 lines
1.8 KiB
TypeScript
/**
|
|
* `/.well-known/security.txt` (RFC 9116).
|
|
*
|
|
* ★ 왜 굽나
|
|
* 지금은 이 경로가 없어서 nginx 의 SPA 폴백이 사장님 앱 index.html 을 200 으로 준다.
|
|
* 진단 도구는 200 을 받고 "security.txt 는 있는데 Contact·Expires 가 없다"로 읽는다 —
|
|
* 404 보다 나쁜 상태다. 파일을 진짜로 놓아 규격대로 답하게 한다.
|
|
*
|
|
* ★ Expires 는 규격상 **필수**이고 지나면 파일이 무효다. 그래서 굽는 시점 기준으로 다시
|
|
* 계산한다 — 발행할 때마다 갱신되므로 사람이 손대야 하는 날짜가 코드에 남지 않는다.
|
|
* 1년이 아니라 180일인 이유: 규격 권고가 "1년 이내"고, 반년이면 발행이 뜸한 호스트도
|
|
* 만료 전에 한 번은 다시 굽힌다.
|
|
*/
|
|
const EXPIRES_DAYS = 180;
|
|
|
|
export interface SecurityTxtOptions {
|
|
/** `mailto:` 또는 `https:` URL. 없으면 호스트의 security@ 주소로 만든다. */
|
|
contact?: string;
|
|
/** 굽는 시각. 테스트가 고정값을 넣는다. */
|
|
now?: Date;
|
|
}
|
|
|
|
export function renderSecurityTxt(origin: string, options: SecurityTxtOptions = {}): string {
|
|
const base = origin.replace(/\/+$/, '');
|
|
const host = base.replace(/^https?:\/\//, '');
|
|
const now = options.now ?? new Date();
|
|
const expires = new Date(now.getTime() + EXPIRES_DAYS * 24 * 60 * 60 * 1000);
|
|
|
|
const contact = (options.contact ?? '').trim() || `mailto:security@${host}`;
|
|
|
|
return [
|
|
`# ${host} — 취약점 제보 창구 (RFC 9116)`,
|
|
`# 이 파일은 발행할 때마다 다시 구워진다. Expires 는 굽는 날 + ${EXPIRES_DAYS}일이다.`,
|
|
'',
|
|
`Contact: ${contact}`,
|
|
`Expires: ${expires.toISOString()}`,
|
|
'Preferred-Languages: ko, en',
|
|
`Canonical: ${base}/.well-known/security.txt`,
|
|
'',
|
|
].join('\n');
|
|
}
|