# Creator Gap Closure — Phase 5 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Close the last four real gaps in the creator prototype, and put on record — with evidence — that most of what the audit listed as Phase 5 work already exists.

**Architecture:** All work lands in `creator/main.dc.html`. Screens are `<sc-if>` blocks gated on `s.route` (or `s.auth`); flags and handlers are properties on the object returned from `Component`'s render method. This phase adds one route, one sheet, and two in-place sections.

**Tech Stack:** Plain HTML + CSS custom properties + the `x-dc` template runtime. Tokens in `creator/_ds/startview-design-system-*/tokens/*.css`. Local server: `python3 server.py`, port 8123.

**Spec:** [`docs/specs/2026-08-19-creator-prototype-gap-closure-design.md`](../specs/2026-08-19-creator-prototype-gap-closure-design.md) §5 Phase 5 — **largely superseded by "Scope correction" below.**

---

## Scope correction (read before starting)

The spec lists ~17 Phase 5 items. Every one was checked against the code and against the repo's decision records. **Eleven are already implemented or are deliberate design choices**; four are real. This is the same pattern as Phases 2 and 3, and the reason is the same: the audit compared against the legacy Angular app rather than against this prototype.

### Already implemented — dropped

| Spec item | Evidence in `creator/main.dc.html` |
|---|---|
| No-search-results state | Two instances — `찾는 결과가 없어요` (~3623) and `검색 결과가 없어요` (~4343) |
| Separate marketing-purpose consent | `agreeRows` (~7034) carries `marketing` / `sms` / `email` as distinct optional rows; `optionalAgreeRows` (~7061) renders them; notification prefs (~5108) list them again |
| Brand-detail section tabs | `brandTab` — 5 references |
| Point-shop purchase confirm | `shopPickId` → `shopSheetOpen` (~7398) drives a purchase sheet |
| Point-shop sort & category | `shopTab` state plus the sheet above |
| Profile completeness gauge | `editProgressPct` / `editProgressLine` / `editProgressBar` (~6823-6825), computed from `editChecks` (~6129) |
| Penalty score | `penaltyTotal` (~7449, renders `-N점`), `penaltyColor`, `penaltyRows` |
| Permanent-suspension messaging | `penaltyNote` (~7451): "2점부터 캠페인 신청이 제한되고, 5점이 되면 영구 정지예요" |

### Deliberate design choices — not gaps

| Spec item | Why it is not a gap |
|---|---|
| Sort and filter **drawer** | The prototype answers this with chips, not a drawer: `sortChips` (~6629, 추천/최신/마감임박) bound at ~1029, plus `catFilter` chips. Community lists additionally have `listSort` (~7635). Converting chips to a drawer is a design decision, not a gap — the same call as the my-campaigns tab question deferred in Phase 2. |
| Inquiry list / detail / **edit** with type selection | The prototype deliberately replaces typed inquiry forms with a chat model: a thread view, an empty state (`문의 내역이 없어요`, ~4290), and a `문의 남기기` CTA. Contacting support §2 asks that the method be clear and appropriate, which chat + help + notices satisfies. Adding a parallel typed-inquiry CRUD would give one job two front doors. |

### Deferred, needs a product call — not planned here

| Item | Why deferred |
|---|---|
| Map view | The audit found it **only in `client-web`**, not in the Angular reference. It needs a real map surface (tiles, pins, permission), which is a substantial build and a different interaction model from every other screen here. Worth doing only if visit-type campaigns are a priority. |
| Advertiser-conversion inquiry (`campaign-register-request`, `consultation-form`, `mall-partner-form`) | These convert a creator into an advertiser. That is the advertiser app's onboarding, and the cross-role spec puts app-boundary questions outside creator flow work. |
| Home banner modal · product preview | Promo/advertiser-side surfaces with no creator journey behind them. Marginal in a prototype whose job is to show the creator's path. |

**Do not re-add any of the above without a decision recorded first.** Two features have already been built and deleted in this programme because an audit entry was trusted over the code.

---

## Global Constraints

- **All edits in `creator/main.dc.html`.** No new files. Nothing under `startview-platform`.
- **Branch:** `creator-phase5`.
- **No test runner exists.** "Verify" means serve the prototype and observe it. Never write pytest/jest steps.
- **Commit messages Korean** (`feat(creator): …` / `fix(creator): …`). No AI attribution, no `Co-Authored-By`, no mention of Claude or AI.
- **Colors/spacing/radius from CSS custom properties only.** Hardcoded hex is a defect except `#fff` on dark button fills.
- **Korean copy**, `class="sv-kr"` on Korean text that *wraps*. Short button labels are conventionally bare.
- **Reuse Phase 0 primitives.** Canonical instances live in the dev gallery (`route:'devStates'`). A near-copy that drifts is a defect; that failure has been caught four times in this programme.

### Verified codebase idioms — do not deviate

1. Back handler for ROUTE screens is `back`; auth screens have their own (`suBack`, `resetBack`, …). There is no `goBack`.
2. No `disabled` attribute anywhere. Guard inside the handler and swap the button's background token.
3. `<sc-for list="{{ rows }}" as="row" hint-placeholder-count="N">`. There is no `each=`.
4. Event handlers take the event: `(e) => … e.target.value`.
5. Partial interpolation inside a style attribute works; whole-attribute `style="{{ obj }}"` also works.
6. `this.after(ms, fn)` for timers, never bare `setTimeout`.
7. Destructive actions go through `askConfirm({title, body, reversible, confirmLabel, onConfirm})`.
8. Row-state changes use a patch helper into component state (`patchApp`, `patchProp`). Never mutate a module-level seed in place.

### Facts this plan relies on (verified — do not re-derive)

- The account-deletion confirm is at `withdrawAsking` (~3535) and today contains only `정말 탈퇴하실까요?` / `그대로 둘게요` / `탈퇴하기`.
- `withdraw()` and `_withdraw` (~7269-7270) perform the deletion; `_withdraw` resets to `SEED` and returns to login.
- `savedBrands` (`{b1:true}`, ~4930) drives the followed-brand list; the saved screen has tabs via `savedTab` (~4932, `캠페인`/`브랜드`).
- `PAST` rows (`pastRows`) hold completed campaigns with `status` and `amount`.
- The attendance screen is `isAttendance` with `attClaimed` (~5491) and a claimed note at ~3144-3149.
- `BRANDS` carries brand records with `id`, and `b1` is the seeded followed brand.

---

## How to verify anything in this plan

Start the server once from the repo root:

```bash
python3 server.py
```

Then for every check:

1. **Byte parity** — `curl -s http://127.0.0.1:8123/creator/main.dc.html | wc -c` must equal `wc -c < creator/main.dc.html`. Use `127.0.0.1`, never `localhost`: a stale IPv6 listener on this port has served a different working copy repeatedly.
2. Navigate to `http://127.0.0.1:8123/creator/main.dc.html?cb=<number>` — the cache-buster is required.
3. First `javascript_tool` call after navigating, paste the accessor:

```js
(function(){
  window.svLogic=function(){const host=document.querySelector('[data-sc-name="main"]')||document.body;
    const key=Object.keys(host).find(k=>k.startsWith('__reactFiber$'));let f=host[key];
    while(f){if(f.stateNode&&f.stateNode.logic&&f.stateNode.logic.setState)return f.stateNode.logic;f=f.return;}return null;};
  var l=window.svLogic(); l.setState({auth:null, tab:'me'}); return {ok:!!l};
})()
```

4. **Read the DOM in a SEPARATE call.** `setState` re-renders asynchronously.
5. **Assert on rendered element text, never `innerHTML.indexOf`** — the page's own `<script>` source is inside `document.body`, so any Korean string that is also a JS literal false-positives.
6. **The console accumulates across same-origin navigations.** An error may be minutes old and already fixed; this cost a full wasted bisect earlier in this programme. Reload with a fresh cache-buster and read the console immediately before believing anything. `/api/comments` 404s are pre-existing noise.
7. **Render-object closures are not methods.** `l.someHandler()` will be `undefined` for anything defined in the render object — drive those by clicking the rendered control instead.

**Known pre-existing, not yours to fix:** a react-dom crash if `channels` is seeded with an incomplete shape; a duplicate `draftBio` key (~6718 vs ~7132) that makes an AI-rewrite handler unreachable.

---

## Task 1: Account-deletion notice

The confirm sheet asks `정말 탈퇴하실까요?` and offers `탈퇴하기` — with no statement of what is destroyed, whether it is reversible, or what would block it. Deleting account §3 requires exactly that, before the confirm. This is the only checklist violation left in the prototype.

**Files:**
- Modify: `creator/main.dc.html` — withdraw sheet markup (~3535), render object

**Interfaces:**
- Consumes: `withdrawAsking` (~7228), `withdraw`/`_withdraw` (~7269-7270)
- Produces: render flags `withdrawLoses` (array of `{label}`), `withdrawBlocked`, `withdrawBlockNote`

- [ ] **Step 1: State the observable check**

The deletion sheet lists what is destroyed and says plainly that it cannot be undone, before the confirm button. If the creator has anything in flight, it says so and the confirm is inert.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "withdrawLoses" creator/main.dc.html
```

Expected: `0`. Read the sheet at ~3535 and confirm it carries no destruction notice.

- [ ] **Step 3: Add the flags**

In the render object, beside `withdrawAsking` (~7228):

```js
      // 무엇이 사라지는지 먼저 말한다 — 누른 뒤에 알려주면 이미 늦다.
      withdrawLoses: [
        { label: '포인트 ' + (s.points || 0).toLocaleString() + 'P — 환불되지 않아요' },
        { label: '진행한 협업 기록과 리뷰 이력' },
        { label: '연결한 채널과 프로필' },
        { label: '정산 계좌와 지급 내역' },
      ],
      // 진행 중인 협업이 있으면 탈퇴가 막힌다 — 브랜드 쪽에 남은 의무가 있어서다.
      withdrawBlocked: (s.applications || []).some((a) => MY_TURN.has(a.status) || a.status === '검수중'),
      withdrawBlockNote: (s.applications || []).some((a) => MY_TURN.has(a.status) || a.status === '검수중')
        ? '진행 중인 협업이 끝나야 탈퇴할 수 있어요.' : '',
```

`MY_TURN` is the existing set at ~5014 — read it before using it and confirm the statuses it holds.

- [ ] **Step 4: Rewrite the sheet body**

Replace the sheet's body between the title and the buttons with the notice. Keep the two existing buttons and their handlers exactly as they are:

```html
        <p class="sv-kr" style="margin-top:8px;font:400 14px/21px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">탈퇴하면 아래 정보가 모두 사라져요.</p>
        <div style="margin-top:14px;padding:14px;border-radius:var(--radius-md);background:var(--surface-1)">
          <sc-for list="{{ withdrawLoses }}" as="w" hint-placeholder-count="4">
          <div style="display:flex;align-items:flex-start;gap:8px;padding:5px 0">
            <span style="flex:none;width:4px;height:4px;border-radius:50%;background:var(--text-tertiary);margin-top:8px"></span>
            <span class="sv-kr" style="flex:1;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">{{ w.label }}</span>
          </div>
          </sc-for>
        </div>
        <p class="sv-kr" style="margin-top:12px;font:500 13px/19px var(--font-body-medium),var(--font-fallback-kr);color:var(--error)">되돌릴 수 없어요.</p>
        <sc-if value="{{ withdrawBlockNote }}" hint-placeholder-val="{{ true }}">
        <p class="sv-kr" style="margin-top:8px;font:500 13px/19px var(--font-body-medium),var(--font-fallback-kr);color:var(--text-tertiary)">{{ withdrawBlockNote }}</p>
        </sc-if>
```

Per Deleting account, the copy states consequences without guilt-tripping. Do not add retention pleading ("정말요?", "아쉬워요") — the checklist calls that out explicitly.

- [ ] **Step 5: Guard the confirm**

Add `if (blocked) return;` at the top of the confirm handler, following the house idiom, and swap the confirm button's background to `var(--surface-2)` with `var(--text-tertiary)` text when `withdrawBlocked`. Do not introduce a `disabled` attribute.

- [ ] **Step 6: Confirm it passes**

Open the deletion sheet from settings. Confirm the four destruction lines render, the irreversibility line is present, and the confirm works. Then set an application to a `MY_TURN` status and confirm the block note appears and the confirm button no longer proceeds — read `auth` before and after the tap to prove it.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 탈퇴 안내 — 사라지는 정보와 되돌릴 수 없음을 먼저 말한다"
```

---

## Task 2: My review activity (video collection)

Completed campaigns leave finished videos, and a creator has nowhere to see them together. The existing product calls this 내 리뷰 활동.

**Files:**
- Modify: `creator/main.dc.html` — render object, new markup, MY menu entry

**Interfaces:**
- Consumes: `PAST` / `pastRows`; the `sv-empty` "nothing yet" pattern; `sv-row` idiom
- Produces: route `'videos'`; flags `isVideos`, `videoRows`, `videosEmpty`, `videoCount`; handler `openVideos()`

- [ ] **Step 1: State the observable check**

A screen reachable from MY lists the videos from completed campaigns, each with its campaign and month, and shows the empty state when there are none.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "isVideos" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add flags and handler**

```js
      isVideos: !s.auth && s.route === 'videos',
      // 완료된 협업만 영상이 남는다 — 미선정·취소에는 결과물이 없다.
      videoRows: (s.pastRows || []).filter((r) => r.status === '완료').map((r) => {
        const o = this.offer(r.offerId) || {};
        return { id:r.id, brand:o.brand || '', title:o.title || '', thumb:o.thumb || '',
          month:r.month, amountLine: r.amount ? r.amount.toLocaleString() + '원' : '' };
      }),
      videosEmpty: !(s.pastRows || []).filter((r) => r.status === '완료').length,
      videoCount: (s.pastRows || []).filter((r) => r.status === '완료').length + '개',
      openVideos: () => this.push('videos', {}),
```

- [ ] **Step 4: Add the markup**

A two-column grid of thumbnails, using the canonical empty state when there is nothing:

```html
    <!-- ─────────── 내 리뷰 활동 ─────────── -->
    <sc-if value="{{ isVideos }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--canvas);z-index:30;display:flex;flex-direction:column;animation:sv-push 350ms cubic-bezier(0.32,0.72,0,1)">
      <div style="display:flex;align-items:center;gap:10px;padding:14px 16px 0">
        <button onClick="{{ back }}" style="display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="arrow-left" variant="line" size="22px" color="var(--ink)" hint-size="22px,22px"></x-import></button>
        <span class="sv-kr" style="flex:1;font:500 17px/23px var(--font-display-medium),var(--font-fallback-kr);color:var(--ink)">내 리뷰 활동</span>
        <span style="flex:none;font:500 13px/1.2 var(--font-body-medium);color:var(--text-tertiary)">{{ videoCount }}</span>
      </div>
      <div class="sv-scroll" style="flex:1;padding:16px 16px 110px">
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
          <sc-for list="{{ videoRows }}" as="v" hint-placeholder-count="4">
          <div>
            <img src="{{ v.thumb }}" alt="" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:var(--radius-xl);display:block">
            <span class="sv-kr" style="display:block;margin-top:8px;font:600 13px/17px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">{{ v.brand }}</span>
            <span class="sv-kr" style="display:block;margin-top:2px;font:400 12px/16px var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">{{ v.month }} · {{ v.amountLine }}</span>
          </div>
          </sc-for>
        </div>
        <sc-if value="{{ videosEmpty }}" hint-placeholder-val="{{ true }}">
        <div style="display:flex;flex-direction:column;align-items:center;text-align:center;padding:48px 24px">
          <span style="width:48px;height:48px;border-radius:var(--radius-circle);background:var(--surface-1);display:flex;align-items:center;justify-content:center"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="task" variant="line" size="22px" color="var(--text-tertiary)" hint-size="22px,22px"></x-import></span>
          <span class="sv-kr" style="margin-top:12px;font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">아직 완료한 협업이 없어요</span>
          <span class="sv-kr" style="margin-top:6px;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">협업을 마치면 만든 영상이 여기에 모여요.</span>
        </div>
        </sc-if>
      </div>
    </div>
    </sc-if>
```

- [ ] **Step 5: Add the MY entry point**

Add a row in the MY space menu beside the existing 지난 캠페인 / 페널티 rows, bound to `{{ openVideos }}`. Read the neighbouring rows first and match their construction exactly — reachability is part of the task, and screens reachable only by console `setState` have been sent back twice in this programme.

- [ ] **Step 6: Confirm it passes**

From MY, click into 내 리뷰 활동. Confirm the completed campaigns appear with their brand, month and amount, and that the count in the header matches the number of tiles. Then set `pastRows:[]` and confirm the empty state replaces the grid.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 내 리뷰 활동 — 완료한 협업의 영상 모음"
```

---

## Task 3: AlimTalk opt-in for followed brands

Following a brand is silent today. The existing product lets a creator opt into AlimTalk so a followed brand's new campaign reaches them, with an explicit consent step because it is an outbound messaging channel.

**Files:**
- Modify: `creator/main.dc.html` — `state`, saved-screen markup, render object

**Interfaces:**
- Consumes: `savedBrands` (~4930), `savedTab` (~4932), `askConfirm`
- Produces: state `brandAlim` (object keyed by brand id); flags `savedBrandRows` gains `alimOn` and `onToggleAlim`

- [ ] **Step 1: State the observable check**

Each followed brand row carries an alarm toggle. Turning it ON asks for consent first, stating what will be sent; turning it OFF is immediate.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "brandAlim" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add state**

In `state = {`, beside `savedBrands`:

```js
    brandAlim:{},
```

- [ ] **Step 4: Extend the followed-brand rows**

Find where the saved screen builds its brand rows (search for `savedBrandList`, ~6136) and add to each row:

```js
        alimOn: !!s.brandAlim[b.id],
        alimLabel: s.brandAlim[b.id] ? '알림 받는 중' : '알림 받기',
        // 켜는 건 동의가 필요하고, 끄는 건 아니다 — 바깥으로 나가는 메시지라서.
        onToggleAlim: () => (s.brandAlim[b.id]
          ? this.setState((st) => ({ brandAlim: { ...st.brandAlim, [b.id]: false } }))
          : this.askConfirm({
              title: b.name + ' 알림을 받을까요?',
              body: '이 브랜드가 새 캠페인을 열면 카카오 알림톡으로 알려드려요. 광고성 정보는 보내지 않아요.',
              reversible: true, confirmLabel: '알림 받기',
              onConfirm: () => this.setState((st) => ({ brandAlim: { ...st.brandAlim, [b.id]: true } })) })),
```

`reversible:true` is correct — the creator can turn it off again.

- [ ] **Step 5: Add the control to the row markup**

Add a small pill control to each followed-brand row bound to `{{ b.onToggleAlim }}` showing `{{ b.alimLabel }}`, with the ON state using `var(--status-picked-bg)` and the OFF state `var(--surface-2)`. Read the existing row markup first and match its spacing.

- [ ] **Step 6: Confirm it passes**

From MY → 찜 → the brand tab, toggle a brand's alarm ON: the confirm sheet must appear and state what will be sent. Dismiss and confirm the toggle is still OFF. Confirm, and check the label flips to `알림 받는 중`. Toggle OFF and confirm it happens immediately with no sheet.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 찜한 브랜드 알림톡 수신 동의 — 켤 때만 동의를 받는다"
```

---

## Task 4: Attendance reward history

The attendance screen shows today's gift box but not what was collected before, so a creator cannot tell what the streak has actually earned.

**Files:**
- Modify: `creator/main.dc.html` — seed data, attendance markup, render object

**Interfaces:**
- Consumes: `isAttendance`, `attClaimed` (~5491); `pointLog` if it already carries attendance entries — check first
- Produces: seed `ATT_LOG` (only if `pointLog` does not already cover it); flags `attLogRows`, `attTotal`

- [ ] **Step 1: State the observable check**

The attendance screen shows a short history of previous rewards with their dates and amounts, plus a total.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "attLogRows" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Check for existing data before adding any**

`pointLog` already records point events (`{id, type, title, point, date}`). Grep it for attendance entries. **If it already carries them, derive the history from `pointLog` and add no new seed** — a second source of truth for the same points would let the two disagree. Only if it does not, add:

```js
/** 출석 보상 기록. 포인트 내역과 같은 사건이므로 금액·날짜 표기를 맞춘다. */
const ATT_LOG = [
  { id:'at1', date:'2026.08.18', point:100 },
  { id:'at2', date:'2026.08.17', point:100 },
  { id:'at3', date:'2026.08.16', point:300, note:'7일 연속' },
  { id:'at4', date:'2026.08.15', point:100 },
];
```

Say in your report which source you used and why.

- [ ] **Step 4: Add the flags**

```js
      attLogRows: /* from pointLog if available, else ATT_LOG */ .map((r) => ({
        ...r, pointLine: '+' + r.point.toLocaleString() + 'P', hasNote: !!r.note })),
      attTotal: /* same source */ .reduce((n, r) => n + r.point, 0).toLocaleString() + 'P',
```

Fill both from whichever source Step 3 selected — do not leave the comment in the shipped code.

- [ ] **Step 5: Add the markup**

Below the gift box, inside the attendance screen:

```html
      <div style="margin-top:26px;padding:0 16px">
        <div style="display:flex;align-items:baseline;gap:8px">
          <h2 class="sv-kr" style="flex:1;font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr);color:#fff">받은 보상</h2>
          <span style="flex:none;font:700 15px/20px var(--font-display-bold);color:var(--point-gold)">{{ attTotal }}</span>
        </div>
        <div style="margin-top:12px">
          <sc-for list="{{ attLogRows }}" as="a" hint-placeholder-count="4">
          <div style="display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid rgba(255,255,255,0.08)">
            <span style="flex:1;font:400 13px/1.3 var(--font-body);color:rgba(255,255,255,0.62)">{{ a.date }}</span>
            <sc-if value="{{ a.hasNote }}" hint-placeholder-val="{{ true }}">
            <span class="sv-kr" style="flex:none;padding:3px 8px;border-radius:var(--radius-xs);background:rgba(255,255,255,0.12);font:600 11px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">{{ a.note }}</span>
            </sc-if>
            <span style="flex:none;font:600 13px/1.2 var(--font-body-semibold);color:#fff">{{ a.pointLine }}</span>
          </div>
          </sc-for>
        </div>
      </div>
```

The attendance screen has a dark background, which is why this block uses white and `rgba(255,255,255,…)` rather than the usual ink tokens — match the surrounding treatment (see `attClaimedNote` at ~3149) rather than introducing light-surface tokens here.

- [ ] **Step 6: Confirm it passes**

Open the attendance screen from MY. Confirm the history renders with dates and amounts, the total equals the sum of the rows, and the streak badge appears on the row that has one. Confirm the text is legible against the dark background.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 출석 보상 내역 — 지금까지 받은 보상과 합계"
```

---

## Phase 5 gate

Walk these by clicking, not by setting state:

- **Deletion** — settings → 탈퇴 → the sheet lists what is lost and says it cannot be undone; with an in-flight collaboration the block note shows and the confirm does not proceed.
- **Review activity** — MY → 내 리뷰 활동 → completed campaigns render; count matches; empty state appears with no completed rows.
- **Brand alarm** — MY → 찜 → brand tab → toggling ON asks consent and states what is sent; dismissing leaves it off; toggling OFF is immediate.
- **Attendance** — MY → 출석 → history renders with a correct total.

Then `read_console_messages {"onlyErrors":true}` on each surface, reading immediately after a fresh cache-busted load. Only `/api/comments` 404s are acceptable.

---

## Self-review notes

**Spec coverage.** Spec §5 Phase 5 lists ~17 items. Eight are already implemented, two are deliberate design choices, and four are deferred pending a product call — all documented with evidence in "Scope correction". The remaining four are Tasks 1–4.

**After this plan.** Phases 0, 1, 2, 3 and 5 will be complete; Phase 4 is cancelled. What remains from the audit is the deferred set above (map view, advertiser-conversion inquiry, inquiry CRUD, banner/preview) plus two known defects outside this programme's scope: the duplicate `draftBio` key, and the advertiser prototype's four stale `수요조사` references.

**Naming consistency.** `askConfirm` with `onConfirm` is consumed by Tasks 1 and 3. The `sv-empty` "nothing yet" pattern is instantiated by Task 2. `patchApp`-style immutable state updates are used by Task 3's `brandAlim`. No task produces an interface another consumes, so all four are independent and may be reordered.

**Known soft spots.**
- Task 1's `withdrawBlocked` predicate uses `MY_TURN` plus `검수중`; the exact set of blocking statuses is a product judgement, and the task tells the implementer to read `MY_TURN` before relying on it.
- Task 4 deliberately defers the data-source decision to the implementer with an explicit rule (prefer `pointLog` if it already carries attendance), because a duplicate source would let the two disagree.
- Task 3 stores `brandAlim` as a flat map keyed by brand id, matching `savedBrands`. If followed brands ever move off that shape, both need updating together.
