사장님 지시: "이메일 승인후에 블로그가 올라가는 페이지에 5초 정도 이후에 연결". 승인만 하고 실제로 어디 올라갔는지 못 찾는 걸 줄인다. 재발행(BUILD 잡)은 몇 분 걸리므로 5초 뒤 반영을 보장하진 않지만, 어디로 가면 되는지는 바로 알려준다. 자동 이동을 못 믿어도 되게 같은 주소를 안내 문구의 링크로도 남긴다. - post_service._blog_url: 발행된 사이트가 있으면 그 미니블로그 자리(#blog) 주소, 없으면 None — 호출부가 자동 이동 없이 확인 문구만 보여준다 - router/v1/site/post.py: <meta http-equiv="refresh"> + 안내 링크 추가. redirect_url은 서버가 site_payload.publish_url()로 만드는 고정 오리진+slugify 통과 값이라 사용자 입력은 아니지만, HTML 속성에 꽂는 자리라 html.escape 적용 (자동 보안 리뷰 지적 반영) - 신규 테스트 2건(발행 사이트 있을 때/없을 때), 관련 스위트 전체 49 passed 로컬 solution-backend·solution-worker 재빌드해 반영 확인함
263 lines
21 KiB
Markdown
263 lines
21 KiB
Markdown
# 미니 블로그 — AI 자동 포스트 생성기 (2026-09-16 기획, 2026-09-17 검수 흐름 개편)
|
|
|
|
숙소 소개 아래에 붙는 짧은 글 게시판. 사장님에게 최종 결정권이 있다 — **팀 사전검수 단계는
|
|
없다.** 메일 링크는 여전히 로그인 없이 쓰고, 대신 빌더 앱에 로그인하면 이번 달 생성된 글
|
|
전체를 볼 수 있다.
|
|
|
|
```
|
|
스케줄러(한 달치 생성) → 금칙 필터(자동) → 메일 발송(업장당 하루 한 통, 승인·수정 두 링크)
|
|
→ 사장님이 승인(즉시 게재) / 수정(빌더 앱 자동 로그인 모달) → 재발행 → 정적 HTML에 글 추가
|
|
※ 두 링크 다 그날 자정(KST) 만료 — 그 뒤엔 로그인해서 빌더 앱에서 처리
|
|
|
|
(병행) 빌더 앱 로그인 → 블로그 글 화면(탭: 이번 주 · 달력 · 생성 이력) → 언제든 수정·승인
|
|
```
|
|
|
|
## 확정된 것
|
|
|
|
- 스테이 DB의 숙소 정보로 **140~150자** 홍보 문구를 AI가 만든다 (2026-09-16)
|
|
- **텍스트만.** 사진은 넣지 않는다 (2026-09-16)
|
|
- 숙소 소개 하단 **미니 블로그** 형식, 글이 쌓이면 **페이지 번호**로 넘긴다 (2026-09-16)
|
|
- 갈래를 나눠 생성하고 **이전에 다룬 주제와 중복되지 않게** 한다 (2026-09-16)
|
|
- ★ **팀 사전검수 폐지** — 검수는 사장님이 한다. 금칙 필터(자동)를 통과하면 바로 발송
|
|
대상이다 (2026-09-17)
|
|
- ★ **한 달치를 미리 쌓아 두고, 업장당 하루 한 통씩** 메일로 내보낸다 (2026-09-17)
|
|
- ★ 메일의 **승인** 링크는 로그인 없음(토큰이 신원) — 누르는 즉시 승인된다(2026-09-17,
|
|
사장님 지시: "승인은 바로 승인 되게 그 링크만 클릭하면"). **수정** 링크는 반대로
|
|
로그인 흐름이다 — 그날짜리 자동 로그인 토큰을 실어 보내 빌더 앱의 편집 모달을 그대로
|
|
연다(2026-09-17, 사장님 지시: "수정하기는 해당 수정하기 페이지로 가게(모달) 로그인도
|
|
크레덴셜로 자동으로 되게"). **두 링크 다 그날 자정(KST) 만료**(2026-09-17, 사장님 지시:
|
|
"승인이랑 수정모두 자정에 만료") — 넘기면 로그인해서 빌더 앱에서 처리한다
|
|
- ★ 사장님이 문구를 **직접 고쳐서** 승인할 수 있다 — 메일의 수정 링크, 빌더 앱에서도 동일
|
|
(2026-09-17)
|
|
- ★ 글마다 **배정일(scheduled_date)** 이 있다 — "언제 만들어졌나"만 있고 "언제 낼 것인가"가
|
|
없으면 달력 화면이 근거 없는 날짜를 지어내야 한다(2026-09-17). 생성 시 그 업장의 다음
|
|
빈 날부터 하루 한 건씩 순서대로 배정한다
|
|
|
|
## 1. 데이터 — 표 하나
|
|
|
|
`postgres-init/init-data/init.sql` 과 `postgres-init/migrations/` **둘 다** 고친다.
|
|
|
|
| 칸 | 타입 | 무엇 |
|
|
|---|---|---|
|
|
| `post_id` | uuid pk | |
|
|
| `place_id` | uuid | 어느 업장 |
|
|
| `body` | varchar(400) | 본문 140~150자 |
|
|
| `topic_kind` | smallint | weather · festival · season · nearby · guide |
|
|
| `topic_key` | varchar(120) | 축제 id · 절기 · 장소 id — **중복 방지의 축** |
|
|
| `status` | smallint | DRAFT → REVIEWED → SENT → APPROVED → PUBLISHED / SKIPPED |
|
|
| `scheduled_date` | date | 이 업장 몫 배정일(KST). 하루 한 통 — 생성 시 순서대로 채운다 (2026-09-17) |
|
|
| `generation_meta` | jsonb | 생성 이력 상세 — 지금은 `{"model": "..."}` 하나뿐(사장님 지시: "생성이력도 상세하게
|
|
기록해놓으셈 어느 모델썼는지 등등" → "Jsonb 하나팟거 컬럼", 새 컬럼을 안 늘리고 여기 얹는다, 2026-09-17) |
|
|
| `approve_token_hash` | varchar(64) | sha256. 평문은 메일에만 |
|
|
| `token_expires_at` | timestamptz | 발송 당일 자정(KST) — 수정 링크(day-pass)도 동일(2026-09-17, 이전엔 발송+14일) |
|
|
| `sent_at` · `approved_at` · `published_at` | timestamptz | |
|
|
| `published_version_id` | uuid | `site_versions` 참조 — 롤백 때 필요 |
|
|
|
|
유니크: `(place_id, topic_key)` — 같은 축제로 두 번 쓰지 않는다.
|
|
유니크: `(place_id, scheduled_date)` — 같은 업장이 같은 날짜를 두 번 차지하지 않는다.
|
|
|
|
## 2. 생성 — 스케줄러 잡
|
|
|
|
`scheduler/__init__.py` 에 `add_job` 한 줄, 로직은 `scheduler/jobs.py` → `services/blog_service.py`.
|
|
|
|
- **주기**: 하루 1회(KST 새벽 04:10). `SCHEDULER_ENABLED=1` 인 프로세스에서만 돈다(이미 그 규약이다)
|
|
- **대상**: 발행된 사이트 중 재고(DRAFT+REVIEWED)가 `REFILL_BELOW`(30) 미만인 업장 —
|
|
하루 한 통씩 나간다고 보면 한 달치를 채우는 셈이다
|
|
- **한 번에 `BATCH_SIZE`(30)건**씩. 앞 회차의 `topic_key` 목록을 프롬프트에 넣어 중복을 막는다
|
|
(한 달치를 한 호출로 뽑으면 중복 검사가 안 된다)
|
|
- **배정일**: 그 업장의 `MAX(scheduled_date)` 다음날부터(없으면 오늘부터, KST) 하루 한 건씩
|
|
순서대로(`blog_jobs._next_scheduled_date`가 아니라 인라인 계산 — `_generate_for_place`).
|
|
"지금 생성하기"(수동 트리거)는 사장님이 직접 고른 구간을 채운다 — 같은 소재 선별·게이트
|
|
로직을 재사용하지만 배정일이 "다음날부터 자동"이 아니라 "그 구간"이다(`generate_range`,
|
|
7절)
|
|
- **갈래 분기**: 날씨·축제·계절·주변장소·이용안내. 갈래마다 프롬프트가 다르고,
|
|
근거가 되는 값도 다르다(날씨=`local.weather`, 축제=`local.festivals`, 주변=`local.attractions`)
|
|
- 프롬프트는 `shared/src/lib/section-prompts.ts` 규약을 따른다 → `npm run export:prompts`
|
|
|
|
### 게이트를 통과하는 문구만 만든다 — 팀 검수를 대신하는 자리
|
|
|
|
발행 게이트 규칙 1은 **미검증 fact 를 화면에 내지 않는 것**이다(`services/publish_gate.py`).
|
|
홍보 문구가 가격·시설·운영시간을 주장하면 그 주장을 뒷받침할 fact 가 없어 규칙과 부딪힌다.
|
|
★ 팀 사전검수가 없어진 지금, 이 필터가 유일한 자동 관문이다.
|
|
|
|
- 프롬프트에 금칙을 건다: 숫자로 된 가격·시간·인원·전화번호를 쓰지 않는다
|
|
- 생성 뒤 기계로 한 번 더 거른다(`blog_service.is_publishable_body`) — 통과하면 곧장
|
|
`REVIEWED` 로 쌓인다(사람이 올릴 필요 없음). 실패분은 로그만 남고 버려진다
|
|
- 통과한 문구는 **고유 콘텐츠**라 오히려 규칙 2(고유 콘텐츠 ≥ 1)에 보탬이 된다
|
|
- 수정 화면(메일·빌더 앱 공통)에서 사장님이 고친 본문도 저장 전에 **같은 필터**를 다시 탄다 —
|
|
로그인했다고 우회되지 않는다
|
|
|
|
## 3. 검수 — 사장님이 한다 (2026-09-17, 팀 사전검수 폐지)
|
|
|
|
★ `admin`(:9801)의 1차 검수 화면(`blog_admin.py`·`BlogReviewPage`)은 삭제했다. 최종 판단은
|
|
사장님 몫이고, 그 판단은 두 군데서 이뤄진다.
|
|
|
|
1. **메일** — 업장당 하루 한 통, 승인/수정/넘기기 (4·5절)
|
|
2. **빌더 앱 로그인** — 이번 달 생성된 글 전체를 미리 보고 메일이 오기 전에 바로
|
|
승인·수정할 수 있다 (7절 "빌더 앱 화면")
|
|
|
|
## 4. 발송 — 메일
|
|
|
|
`services/mail_service.py`(2026-09-16 완성, ACS 우선 · SMTP 폴백)를 그대로 쓴다.
|
|
|
|
- **업장당 하루 한 통.** `PostCRUD.due_for_mail` 이 `scheduled_date <= 오늘` 이면서
|
|
`DISTINCT ON (place_id)` 로 업장 하나가 밀려 있어도 그날은 가장 이른 배정일 한 통만
|
|
고른다(`blog_jobs.send_reviewed`) — 미래 배정일 글은 그날이 오기 전엔 안 나간다
|
|
- 본문: 문구 전문 + 승인 링크 + 수정 링크(`blog_jobs._mail_body`)
|
|
- **승인 링크**: `GET /v1/site/post/approve?t=<토큰>` — 로그인 없음, 토큰이 신원. **누르는
|
|
즉시 승인된다**(확인 화면 없음, 2026-09-17 사장님 지시). 토큰은 32바이트 랜덤 → DB 엔
|
|
sha256 만, **단회용 · 그날 자정(KST) 만료**(`blog_service.issue_token`). 승인 확인
|
|
화면은 그 업장의 발행된 사이트(미니 블로그 자리, `#blog`)로 5초 뒤 자동 이동한다
|
|
(2026-09-22 사장님 지시 — `router/v1/site/post.py _page`, `PostService._blog_url`).
|
|
재발행(BUILD 잡)은 몇 분 걸리므로 5초 뒤에 이 글이 이미 반영돼 있다는 보장은 없다 —
|
|
그래도 "어디로 가면 보이는지"는 바로 알려준다. 발행된 사이트가 없으면 자동 이동 없이
|
|
확인 문구만 보여준다
|
|
- **수정 링크**: `{origin}/blog?placeId=&postId=&auto=<그날짜리 JWT>` — 로그인 흐름이다.
|
|
`CreateDayPassToken`(`router/v1/validator/dependencies.py`)이 자정까지만 사는 접근
|
|
토큰을 찍고, 빌더 앱이 그 토큰으로 로그인해 그 글의 편집 모달을 바로 연다
|
|
(`solution/frontend/src/app/provider.tsx` 세션 복구 단계에서 처리 — `BlogPostsPage` 안이
|
|
아니라 라우트 가드보다 먼저인 지점이어야 한다, 2026-09-17 실측: 늦게 처리하면
|
|
`RequireAuth` 가 이미 `/login` 으로 튕긴 뒤였다)
|
|
- 메일은 평문으로 흐른다 → 승인 링크로 할 수 있는 일은 **그 글 한 건의 게재**뿐이고,
|
|
수정 링크로 할 수 있는 일은 **그 글 한 건의 편집·승인**뿐이다(day-pass 토큰도 `user_id`
|
|
까지만 담아, 그 사장님의 다른 글은 못 건드리지 않는다 — `PostService.get_post` 가
|
|
`place_id` 불일치를 걸러낸다)
|
|
|
|
## 5. 승인·수정
|
|
|
|
- **게재**: 이메일의 승인 링크(로그인 없음, 누르면 즉시 승인) 또는 빌더 앱에 로그인해
|
|
"바로 발행" 버튼을 눌러도 승인된다(2026-09-21, 사장님 지시: "이메일 승인으로도 발행
|
|
가능하고 바로발행버튼으로도 발행 가능하도록") — 두 경로 다 열려 있다(`post_service.
|
|
PostService._approve_and_publish`). PUT(수정)은 저장만 하고 자동으로 승인하지 않는다.
|
|
- **승인**: 이메일 GET 은 로그인 없이 즉시 승인, "바로 발행" 은 로그인 세션이 신원 →
|
|
둘 다 `status = APPROVED` → BUILD 잡 큐. 이메일 링크의 만료·재사용은 "처리할 수 없는
|
|
링크입니다" 안내로 끝낸다(오류 화면을 주지 않는다)
|
|
- **쓰레드 연동**: 승인되는 순간(경로 무관) 그 업장이 쓰레드에 연결돼 있으면 같은 문구에
|
|
발행 링크를 붙여 쓰레드에도 즉시 게시한다(2026-09-21) — 별도 승인 없음(`docs/DECISIONS.md`
|
|
7-1-2 개정, `docs/SOCIAL.md`). 연동 안 돼 있거나 `SOCIAL_POSTING_ENABLED=0`이거나 사이트
|
|
domain이 미확정이면 조용히 건너뛴다. 실패해도 미니블로그 승인 자체는 막지 않는다
|
|
(`post_service.PostService._try_social_share`)
|
|
- **수정**: 빌더 앱 편집 모달에서 저장 → `is_publishable_body` 재검사 → 통과 시 본문만
|
|
갱신한다. **승인 전환은 하지 않는다** — 실패하면 사유를 보여주고 다시 고치게 한다,
|
|
통과해도 두 승인 경로 중 하나를 눌러야 사이트에 반영된다
|
|
- **알림 이메일**: 승인 메일 수신자는 `places.notify_email`(비면 `users.email`) —
|
|
계정 로그인 이메일과 분리해서 업장별로 다른 담당자에게 보낼 수 있다(빌더 앱 미니블로그
|
|
관리 화면에서 수정, `PATCH /v1/place/{place_id}`)
|
|
- ★ BUILD 잡 payload 에는 반드시 `owner_user_id` 가 있어야 한다(`build_service.run_build`
|
|
가 `payload["owner_user_id"]` 를 무조건 읽는다) — 토큰/day-pass 흐름은 일반 로그인
|
|
세션과 달라 `post_service.PostService._enqueue_build` 가 `place_id` 로 직접 조회해
|
|
채운다. 이게 빠져 있던 게 2026-09-17 발견된 버그였다(회귀 테스트: `test_blog_post.py
|
|
test_approve_enqueues_build_with_owner_user_id`)
|
|
|
|
## 6. 게재 — 재발행
|
|
|
|
`docs/PUBLISH_VERSION.md` 의 파이프라인을 그대로 탄다. payload 에 `posts[]` 를 실어
|
|
**그 사이트 하나만** 다시 굽고 새 버전으로 링크를 전환한다. 전체 재굽기가 아니다.
|
|
|
|
⚠️ **발행일(`publishedAt`)이 움직인다.** 글 한 건 때문에 사이트 갱신일이 바뀌는 것이
|
|
맞는지 합의가 필요하다 — 색인에는 유리하지만 "사장님이 발행한 적 없는데 날짜가 바뀐다"는
|
|
기존 원칙과 부딪힌다.
|
|
|
|
## 7. 화면
|
|
|
|
### 발행된 사이트 — 미니 블로그
|
|
|
|
`solution/site/src/sections/BlogSection.tsx`, 숙소 소개(`intro`) 바로 아래.
|
|
|
|
- **글 전부가 HTML 안에 있고 JS 가 10건씩 보여준다.** 페이지를 눌렀을 때 더 불러오지 않는다 —
|
|
크롤러는 2페이지를 못 본다
|
|
- 사이트 하나 = 한 장 규칙은 유지한다. 주소를 늘리지 않는다
|
|
- 글이 100건을 넘으면 그때 별도 주소를 다시 논의한다
|
|
- 군산 읽기 전체 노출도 같은 페이지네이션을 쓴다 — 컴포넌트를 한 벌만 만든다
|
|
|
|
### 빌더 앱 — 이번 달 생성된 글 (2026-09-17)
|
|
|
|
`solution/frontend/src/pages/BlogPostsPage.tsx`. "내 사이트" 카드의 **관리 메뉴 →
|
|
미니블로그 관리**에서 `?placeId=` 를 들고 들어온다(전역 메뉴 하나로는 어느 사이트인지
|
|
못 고른다 — 사장님 한 명이 사이트 여럿을 가질 수 있다).
|
|
|
|
- 백엔드: `GET/PUT /v1/place/{place_id}/post`(`router/v1/site/post.py` `owner_router`,
|
|
:9800). 로그인 세션(`IsValidAccessToken`)이 신원이고, `PlaceCRUD.get_place` 로 소유권을
|
|
매번 확인한다 — 토큰 흐름과 인증 방식이 다를 뿐 편집 가드(`is_publishable_body`)는 같다
|
|
- **아직 메일이 안 나간 `REVIEWED` 글도 여기서 바로 승인·수정할 수 있다** —
|
|
`PostCRUD._EDITABLE = (SENT, REVIEWED)`. 메일을 기다릴 필요가 없다
|
|
- 월 단위 조회(`month=YYYY-MM`, 기본 이번 달, KST 기준) — `scheduled_date` 기준으로 그 달에
|
|
배정된 글을 가져온다
|
|
- **화면은 탭 둘뿐이다** (2026-09-17, 사장님 지시: "탭을 왜 이번주 달력 이렇게 나누고
|
|
지랄이야 내가 언제그러라그랬어 달력위에 이번주 카드들 보여주라고 했지" — 카로셀·달력은
|
|
같은 화면에 **항상 같이** 뜬다, "생성 이력"만 별도 탭이다)
|
|
1. **블로그(카로셀 + 달력, 항상 같이 보인다).**
|
|
- **카로셀** — "오늘·내일 등 일주일치를 보기 편하게" 모은 것(사장님 표현). 달력(월
|
|
단위)과 무관하게 **항상 오늘부터 7일치**(`GET .../post/upcoming?days=7`,
|
|
`PostService.list_upcoming`, 날짜 오름차순). 카드가 겹쳐 쌓여 있고 가로로 넘기면
|
|
하나씩 앞으로 나온다(`PostCarousel`). 마우스를 올린 카드는 안 가려지게 z-index 를
|
|
맨 앞으로 올린다. 카드를 누르면 그 자리에서 고치는 게 아니라 **모달**을 연다(사장님
|
|
지시: "카드클릭해도 모달나와서 수정가능하게 해야지 왜 바로수정하게해") — 카드 자체는
|
|
미리보기(`PostPreviewCard`)뿐이고, 수정·바로 발행은 모달 안(`PostCard`)에서만 한다.
|
|
카드마다 배정일을 전부 쓰고, 오늘·내일인 카드에는 그 위에 "오늘"/"내일" chip 을 더 단다
|
|
- **달력** — **이전 달 · 월 · 다음 달** 이 달력 바로 위에 있다(사장님 지시). 이번 달부터
|
|
1년 뒤까지만 넘겨볼 수 있다(그 전·그 뒤는 볼 이유가 없다). 글이 0건이어도 칸은 항상
|
|
뜬다 — 배정일이 없으면 "이 달에 뭐가 있나"를 훑어볼 기준 자체가 없다. 칸마다 본문
|
|
앞부분 스니펫과 **발행완료 · 발행실패 · 발송완료 배지만** 보여준다 — 검수 대기처럼
|
|
아직 메일도 안 나간 상태는 아무 표시도 하지 않는다(사장님 지시: "발행전인건 표시하지
|
|
말고"), 메일 발송 여부는 크론잡이 실제로 돌았다는 확인이라 따로 보여준다(사장님 지시:
|
|
"달력에 발송완료 된거는 되었다고 적으라고"). **칸을 누르면 모달**로 그 글 전체 내용과
|
|
편집·발행 버튼을 보여준다
|
|
- **빈 날짜(오늘 이후만) 개별 생성** (2026-09-17, 사장님 지시: "그리고 개별적으로 새로
|
|
만들수있게 해줘") — 글이 없는 칸을 누르면 `POST .../post/generate-one?date=`
|
|
(`PostService.generate_for_date` → `blog_jobs.generate_one_for_date`)가 그 날짜 하나만
|
|
채운다. 재고 상한(`REFILL_BELOW`)을 안 본다 — 콕 집은 요청이라 상한이 끼어들 자리가
|
|
아니다. 이미 그 날짜에 글이 있으면(유니크 충돌) 조용히 덮지 않고 실패로 답한다.
|
|
지난 날짜는 만들 이유가 없어 클릭 자체를 막는다. 성공하면 그 자리에서 모달이 열린다
|
|
2. **생성 이력.** 언제 몇 건, 어느 모델로 만들었는지(사장님 지시: "생성이력도 있어야해
|
|
몇개 생성했는지" / "생성이력도 상세하게 기록해놓으셈 어느 모델썼는지 등등") —
|
|
`GET .../post/history`(`PostCRUD.generation_batches`). 새 컬럼 없이 기존 `created_at`
|
|
으로 회차를 묶는다(같은 트랜잭션 안의 `add_many` 는 DB `now()` 가 전부 같다). 모델명은
|
|
`generation_meta->>'model'` 의 대표값(`MAX`) 하나 — 한 회차 = 한 모델이 정상이다
|
|
- **발행실패 판정**: `PostService._latest_build_failed` — 그 업장의 가장 최근 BUILD 잡이
|
|
`JobStatus.DEAD`(재시도 소진)면, APPROVED 인데 아직 안 나간 글에 `build_failed=true` 를
|
|
단다. 글 단위가 아니라 "이 업장 재발행이 지금 막혀 있나" 를 보는 것이다 — BUILD 잡 하나가
|
|
그 업장의 승인분 전부를 한 번에 굽기 때문
|
|
- ⚠️ **`scheduled_date` 마이그레이션(0019) 전에 만들어진 글은 그 컬럼이 비어 있다.**
|
|
월별·주간 조회 둘 다 `scheduled_date` 로 거르므로, 비어 있으면 화면 어디에도 안 뜬다
|
|
(실측 2026-09-17: "지금 생성하기"로 만든 실제 글 13건이 이렇게 사라져 보였다). 배포
|
|
직후 한 번은 기존 NULL 행에 날짜를 채우는 백필이 필요하다 — 업장별로 `created_at` 순서를
|
|
살려 오늘부터 하루씩 순서대로 채운다(1회성, 스크립트로 남기지 않았다).
|
|
- **지금 생성하기** 버튼 — `POST /v1/place/{place_id}/post/generate?start=&end=`(사장님 지시:
|
|
"지금 생성하기에서 시작이랑 끝 날짜를 정해야하지 않을까"). 버튼을 누르면 시작일·끝일을
|
|
캘린더 입력(`<input type="date">`)으로 고르는 다이얼로그가 뜬다(사장님 지시: "캘린더
|
|
UI로 날짜받게"). 재고 상한(`REFILL_BELOW`)을 안 본다 — 개별 생성과 같은 이유로, 직접
|
|
고른 구간에 상한 로직이 끼어들 자리가 아니다(`blog_jobs.generate_range`). 이미 글이 있는
|
|
날짜는 LLM 을 부르지 않고 건너뛰고, 구간 안 소재가 떨어지면 그 자리에서 멈춘다 — 응답에
|
|
`requested`(구간 일수)·`created`(실제로 채운 일수)를 같이 줘서 "N일 중 M일만 채웠습니다"로
|
|
보여준다. 발행 전 사업장은 애초에 생성 스윕 대상이 아니라(`_published_places`) 여기서도
|
|
0건이다. domain 이 아직 확정되지 않은(임시 주소) 사이트도 마찬가지다(2026-09-21 —
|
|
쓰레드 연동 요구사항과 맞췄다, `docs/SOCIAL.md` 7-1-1과 동일 기준)
|
|
|
|
## 8. 진행 (2026-09-17)
|
|
|
|
| | 자리 | 상태 |
|
|
|---|---|---|
|
|
| 표 + 마이그레이션 | `migrations/0017_place_posts.sql` · `init.sql` | 완료 |
|
|
| 생성 + 금칙 필터 | `services/blog_service.py` | 완료 |
|
|
| 생성·발송 스윕 | `services/blog_jobs.py` · `scheduler/jobs.py` | 완료 (새벽 4:10 생성 · 아침 9:00 발송, 업장당 하루 한 통) |
|
|
| ~~어드민 검수~~ | ~~`router/v1/site/blog_admin.py`~~ | **폐지(2026-09-17)** — 검수는 사장님이 한다 |
|
|
| 메일 + 승인·수정 | `services/mail_service.py` · `services/post_service.py` · `router/v1/site/post.py` | 완료 |
|
|
| 빌더 앱 로그인 화면(달력) | `router/v1/site/post.py owner_router` · `site/pages/BlogPostsPage.tsx` | 완료 |
|
|
| 배정일(scheduled_date) | `migrations/0019_*.sql` · `blog_jobs._generate_for_place` | 완료 |
|
|
| 생성 이력 상세(모델명, generation_meta) | `migrations/0020_*.sql` · `blog_service.generate_one` | 완료 |
|
|
| 개별 생성(빈 날짜 하나) | `POST .../post/generate-one` · `blog_jobs.generate_one_for_date` | 완료 |
|
|
| payload + 화면 | `site_payload.posts[]` · `site/src/sections/BlogSection.tsx` | 완료 |
|
|
| 재발행 연결 | `build_service` → `mark_published`, `owner_user_id` 버그 수정 | 완료 |
|
|
| 쓰레드 자동 게재 | `services/social_service.publish_reused_text` · `post_service._try_social_share` | 완료 |
|
|
|
|
남은 것: 운영 ACS 에 발신 도메인 등록(지금은 negodata 리소스를 빌려 쓴다),
|
|
그리고 6절의 발행일 갱신 합의.
|
|
|
|
## 안 하는 것
|
|
|
|
- 사진 첨부 (2026-09-16 회의 확정)
|
|
- 글마다 별도 URL·목록 페이지 — 한 장 규칙을 깬다
|
|
- 예약 요청 관리 화면 — `booking_request.py` 는 요청을 DB 에 남기지 않는다(2026-09-16
|
|
대표 지시). 목록을 만들려면 그 결정부터 바꿔야 한다
|