도메인별 스키마(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>
148 lines
6.3 KiB
CSS
148 lines
6.3 KiB
CSS
@import "tailwindcss";
|
|
@import "tw-animate-css";
|
|
|
|
/* 디자인 토큰(색상·폰트·radius)은 shared 단일 소스에서 관리. site 와 공유 가능. */
|
|
@import "@o2o/shared/styles/tokens.css";
|
|
@import "@o2o/shared/styles/base.css";
|
|
/* 캔버스가 그리는 것은 발행본과 같은 사이트다 — 시안 토큰·유틸을 같은 파일에서 읽는다.
|
|
규칙은 전부 `.site-canvas` 안으로 스코프돼 있어 관리자 크롬에는 닿지 않는다. */
|
|
@import "@o2o/shared/styles/site.css";
|
|
|
|
body {
|
|
background-color: var(--background);
|
|
color: var(--foreground);
|
|
font-family: var(--font-sans);
|
|
transition: background-color 0.2s ease, color 0.2s ease;
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border outline-ring/50;
|
|
}
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
html {
|
|
@apply font-sans;
|
|
}
|
|
}
|
|
|
|
/* 캔버스(발행 사이트 미리보기)는 관리자 토큰이 아니라 템플릿 색을 쓴다.
|
|
.site-canvas 안쪽은 --tpl-* 변수로만 색을 잡는다 — 관리자 크롬 색이 새어 들어오지 않게. */
|
|
@layer components {
|
|
.site-canvas {
|
|
color-scheme: light;
|
|
background-color: var(--tpl-bg, #ffffff);
|
|
color: var(--tpl-text, #09090b);
|
|
/* 본문 서체도 토큰이다. 변수가 없으면(빌더 캔버스 등) 지금까지처럼 관리자 본문 서체를 쓴다. */
|
|
font-family: var(--tpl-font-body, var(--font-sans));
|
|
}
|
|
.site-canvas .serif-title {
|
|
font-family: var(--tpl-font-heading, 'Noto Serif KR', 'Batang', 'Times New Roman', serif);
|
|
}
|
|
/* 캔버스 안의 '면'(카드·패널)과 테두리도 토큰을 따른다.
|
|
★ bg-white / bg-stone-* 이 캔버스 안에만 82곳 흩어져 있어 파일마다 고치는 대신 여기서 덮는다.
|
|
폴백이 원래 Tailwind 값이라, --tpl-* 이 없는 곳에서는 지금까지와 완전히 동일하다.
|
|
★ 팔레트를 바꿔도 화면이 안 변하는 원인의 절반이 이 면들이었다(나머지 절반은 섹션 바탕). */
|
|
.site-canvas .bg-white {
|
|
background-color: var(--tpl-card, #ffffff);
|
|
}
|
|
.site-canvas .bg-stone-50 {
|
|
background-color: var(--tpl-card, var(--color-stone-50));
|
|
}
|
|
.site-canvas .bg-stone-100 {
|
|
background-color: var(--tpl-surface-alt, var(--color-stone-100));
|
|
}
|
|
/* 테두리는 팔레트 색을 그대로 쓰면 너무 세다 — 30% 만 섞는다. */
|
|
.site-canvas .border-stone-200,
|
|
.site-canvas .border-stone-300 {
|
|
border-color: color-mix(in oklab, var(--tpl-border, var(--color-stone-200)) 30%, transparent);
|
|
}
|
|
|
|
/* serif-title 을 안 쓰는 제목도 제목 서체를 따르게 한다.
|
|
★ 폴백이 inherit 이라 --tpl-font-heading 이 없으면 예전과 똑같이 본문 서체를 그대로 물려받는다. */
|
|
.site-canvas h1,
|
|
.site-canvas h2,
|
|
.site-canvas h3,
|
|
.site-canvas h4 {
|
|
font-family: var(--tpl-font-heading, inherit);
|
|
letter-spacing: var(--tpl-heading-tracking, normal);
|
|
}
|
|
/* ★ 굵기는 폴백을 inherit 로 두지 않는다. 간판체(Gugi)는 굵기가 한 벌뿐이라 700 을 주면
|
|
브라우저가 가짜 볼드를 씌워 획이 뭉갠다 — 템플릿이 400 을 지정할 수 있어야 한다. */
|
|
.site-canvas :is(h1, h2, h3, h4).tpl-title {
|
|
font-weight: var(--tpl-heading-weight, 700);
|
|
}
|
|
/* 카드·패널 테두리 두께도 템플릿이 정한다. 레트로는 2px 라야 인쇄물처럼 보인다. */
|
|
.site-canvas .border {
|
|
border-width: var(--tpl-border-width, 1px);
|
|
}
|
|
}
|
|
|
|
/* 히어로 제목의 앞말만 갈아 끼운다 — '홈페이지' 는 고정이고 수식어가 돈다.
|
|
★ CSS 만으로 돈다(마퀴와 같은 이유). 문구를 **일곱 줄** 쌓는다 — 마지막 줄이 첫 줄의
|
|
복제라 -85.714% 에서 0% 로 되감을 때 글자가 튀지 않는다.
|
|
★ 줄 높이를 1.25em 으로 못 박는다. 창이 1줄이고 트랙이 7줄이라 한 칸이 정확히 1/7 이어야 한다.
|
|
★ 문구 개수(6)와 이 키프레임은 한 몸이다. 문구를 늘리면 여기 stop 도 같이 고쳐야 한다 —
|
|
안 고치면 마지막 몇 개가 영영 안 보이거나 빈 줄이 지나간다(LandingPage HEADLINES). */
|
|
@keyframes o2o-rotate-6 {
|
|
0%, 14% { transform: translateY(0); }
|
|
16.67%, 30.67% { transform: translateY(-14.286%); }
|
|
33.33%, 47.33% { transform: translateY(-28.571%); }
|
|
50%, 64% { transform: translateY(-42.857%); }
|
|
66.67%, 80.67% { transform: translateY(-57.143%); }
|
|
83.33%, 97.33% { transform: translateY(-71.429%); }
|
|
100% { transform: translateY(-85.714%); }
|
|
}
|
|
.o2o-rotator {
|
|
display: block;
|
|
height: 1.25em;
|
|
overflow: hidden;
|
|
}
|
|
.o2o-rotator-track {
|
|
/* ★ display:block 이 없으면 아무 일도 안 일어난다. 이 요소는 <span> 이라 기본이 inline 이고,
|
|
**인라인 요소에는 transform 이 적용되지 않는다** — 애니메이션은 걸려 있는데 화면은 정지다. */
|
|
display: block;
|
|
animation: o2o-rotate-6 16s cubic-bezier(0.65, 0, 0.35, 1) infinite;
|
|
}
|
|
.o2o-rotator-track > span {
|
|
display: block;
|
|
height: 1.25em;
|
|
line-height: 1.25;
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.o2o-rotator-track { animation: none; }
|
|
}
|
|
|
|
/* 랜딩 히어로의 발행 사이트 마퀴. **CSS 만으로 돈다** — 타이머를 쓰면 탭이 백그라운드일 때
|
|
프레임이 밀려 끊긴 것처럼 보인다. 트랙에 같은 목록을 두 벌 넣고 -50% 까지 밀면 이음매가 없다. */
|
|
@keyframes o2o-marquee {
|
|
from { transform: translateX(0); }
|
|
to { transform: translateX(-50%); }
|
|
}
|
|
.o2o-marquee-track {
|
|
animation: o2o-marquee var(--marquee-duration, 48s) linear infinite;
|
|
}
|
|
.o2o-marquee:hover .o2o-marquee-track {
|
|
animation-play-state: paused;
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.o2o-marquee-track { animation: none; }
|
|
}
|
|
|
|
/* 수집 진행바 shimmer — 0%(빈 트랙) 구간에서도 '진행 중'이 보이게 한다. */
|
|
@keyframes o2o-shimmer {
|
|
from { transform: translateX(-100%); }
|
|
to { transform: translateX(500%); }
|
|
}
|
|
.o2o-progress-shimmer {
|
|
animation: o2o-shimmer 1.8s ease-in-out infinite;
|
|
}
|
|
|
|
/* 노치/홈 인디케이터 회피 (viewport-fit=cover 와 짝).
|
|
화면 맨 위/맨 아래에 붙는 바에만 붙인다 — 스크롤되는 본문에는 쓰지 않는다. */
|
|
@layer components {
|
|
.safe-t { padding-top: env(safe-area-inset-top); }
|
|
.safe-b { padding-bottom: env(safe-area-inset-bottom); }
|
|
}
|