# 끼리 (KKIRI) — 유저 플로우

작성: 페이블 · 2026-09-04
기준: 목업 `OUTBOX/KKIRI/mockup/app.js` 의 실제 화면 전환 코드(`go()` 호출과 `advance()` 마감 규칙)를 그대로 옮겼다. 화면 ID는 목업 코드의 `state.screen` 값이고, 괄호 안 `S번호`는 화면 시나리오 문서의 번호다. 스크린샷 번호는 검증 로그의 `shots/` 파일과 같다.
※ 목업은 단일 브라우저 시뮬레이션이다. "서버"로 적은 부분은 실제 서비스에서 서버가 맡을 일이며 목업에서는 localStorage 와 시뮬레이터 패널이 대신한다.

---

## 0. 화면 ID 대응표

| 화면 ID (코드) | 시나리오 | 역할 | 스크린샷 | 다음으로 갈 수 있는 곳 |
|---|---|---|---|---|
| `home` | S0 | 방장 | 01 | `setup` |
| `setup` | S1 | 방장 | 02 | `invite` |
| `invite` | S2 | 방장 | 03 · 20 | `join` / `waiting`(이미 답변함) |
| `join` | S3 | 전원 | 04 | `questions` |
| `questions` (q0~q5) | S4 | 전원 | 05 · 06 · 07 | `waiting` / `join`(첫 문항에서 이전) |
| `waiting` | S5 | 전원 | 08 · 21 · 22 | `negotiating` / `questions`(답변 고치기) / `expired` |
| `negotiating` | S6 | 전원 | 09 · 24 · 29 | `result` / `noCandidate` |
| `result` | S7 | 전원 | 10 · 25 · 27 | `receipt` / `ratify` |
| `receipt` | S8 | 본인만 | 11 | `ratify` / `result` |
| `ratify` | S7-b (쏠 S09) | 전원 | 12 · 13 · 14 · 26 · 28 | `done`(방장) / `negotiating`(재조율 1회) / `result` |
| `done` | S9 | 전원 | 15 | `home` |
| `expired` | 예외 | 방장 | 23 | `waiting` 또는 `invite`(연장) / `home`(방 닫기) |
| `noCandidate` | 예외 | 전원 | 30 | `home` |

---

## 1. 전체 흐름 한눈에

해피패스는 굵은 선, 예외는 점선. 마름모는 사람이 고르는 분기, 육각형은 시스템이 자동으로 정하는 분기.

```mermaid
flowchart TD
  home["home · S0<br/>랜딩 · 주제 선택"] ==>|"팀 워크숍 카드"| setup["setup · S1<br/>인원 3~8 · 마감 · 초대 방식 · 방장 이름"]
  setup ==>|"초대방 만들기"| invite["invite · S2<br/>링크 · 코드 · 공유 시트 · 참여 현황"]
  invite ==>|"내 답변 시작하기"| join["join · S3<br/>비공개 약속 · 이름"]
  join ==>|"약속 확인"| q["questions · S4<br/>6문항 (상한 토글 · 1·2순위 · 자유서술)"]
  q ==>|"내 대리인 보내기"| waiting["waiting · S5<br/>내 대리인 카드 · 참여 현황 · 마감"]
  waiting -->|"답변 고치기 (시작 전)"| q
  waiting ==> allDone{{"전원 답변?"}}
  allDone ==>|"예 · 3초 뒤 자동"| nego["negotiating · S6<br/>4단계 + 익명 발언 로그"]
  allDone -->|"아니오"| startNow{"방장:<br/>지금 협상 시작?"}
  startNow -->|"확인 모달 · 미응답 조건 미반영"| nego
  startNow -->|"기다림"| deadline{{"답변 마감 도달"}}
  deadline -.->|"답변 ≥ 3명 · 자동 시작"| nego
  deadline -.->|"답변 < 3명"| expired["expired<br/>아직 시작할 수 없어요"]
  expired -.->|"24h 연장 (최대 2회)"| waiting
  expired -.->|"방 닫기"| home
  nego ==> ok{{"거부권 통과<br/>후보 ≥ 1?"}}
  ok ==>|"예"| result["result · S7<br/>최종안 · 이유 · 양보 · 공정성 · 차선"]
  ok -.->|"아니오"| noc["noCandidate<br/>모든 조건을 지키는 안이 없어요"]
  noc -.->|"상한 완화 익명 동의 요청 / 방 닫기"| home
  result ==>|"내 대리인 리포트 (본인만)"| receipt["receipt · S8<br/>지켜진 조건 · 양보 · 내 만족도"]
  receipt ==>|"최종 의견 보내기"| ratify["ratify<br/>동의 / 조건부 / 재조율 → 익명 집계"]
  result -->|"최종 의견 보내기"| ratify
  ratify --> renego{{"재조율 요청 있음?"}}
  renego -.->|"예 · 방당 1회"| nego
  renego ==>|"아니오 · 전원 응답 또는 24h 마감"| confirm{"방장:<br/>결정 확정"}
  confirm ==> done["done · S9<br/>확정 카드 · 공유 · .ics · 실행 체크"]
  done -->|"새 결정 시작하기"| home

  classDef screen fill:#fff,stroke:#171717,stroke-width:1.5px
  classDef exc fill:#fef2f2,stroke:#b91c1c,stroke-dasharray:4 3
  classDef sys fill:#ecfdf5,stroke:#0f766e
  class home,setup,invite,join,q,waiting,nego,result,receipt,ratify,done screen
  class expired,noc exc
  class allDone,deadline,ok,renego sys
```

---

## 2. 역할별 스윔레인 — 방장 · 참가자 · 시스템

방장은 방을 만들고 시작·확정을 결정한다. 참가자는 답변과 최종 의견만 낸다. 시스템은 마감·리마인드·협상·집계를 맡는다. 방장도 참가자 열의 일을 그대로 한다(자기 답변).

```mermaid
flowchart TB
  subgraph H["방장"]
    direction TB
    h1["주제 선택"] --> h2["인원 · 마감 · 초대 방식 · 이름"] --> h3["초대 링크 공유<br/>카톡 · 슬랙 · 문자 · QR · 복사 · 캘린더"]
    h3 --> h4["참여 현황 확인"]
    h4 --> h5{"전원 완료 전<br/>시작할까?"}
    h5 -->|"예"| h6["확인 모달 → 시작"]
    h5 -->|"아니오"| h7["미응답자 다시 알림<br/>(30분 쿨다운)"]
    h8["재확인 집계 확인"] --> h9["결정 확정하기"]
  end
  subgraph P["참가자 (방장 포함)"]
    direction TB
    p1["링크 · 코드로 입장"] --> p2["비공개 약속 · 이름"] --> p3["6문항 답변<br/>상한 = 거부권"] --> p4["자유서술 구조화 확인"] --> p5["대기실<br/>내 대리인 카드"]
    p6["결과 열람"] --> p7["내 리포트 (본인만)"] --> p8["최종 의견<br/>동의 / 조건부 / 재조율"]
  end
  subgraph S["시스템"]
    direction TB
    s1["방 · 링크 · 코드 생성"] --> s2["리마인드<br/>남은 시간 절반 · 마감 1h 전"] --> s3{{"마감 도달"}}
    s3 -->|"≥3명"| s4["협상 시작<br/>익명 구조화 → 거부권 → 최저 만족도 → 설명"]
    s3 -->|"<3명"| s5["연장 제안 (최대 2회)"]
    s4 --> s6["합의 제안 + 개인 리포트"]
    s6 --> s7["재확인 집계 · 24h 마감<br/>미응답 = 기권"]
    s7 -->|"재조율 1회"| s4
  end
  h2 --> s1
  h3 --> p1
  p5 --> s3
  h6 --> s4
  s6 --> p6
  s6 --> h4
  p8 --> s7
  s7 --> h8
```

---

## 3. 방 상태 머신 — 타임아웃 규칙

방 하나의 수명. 모든 시간은 목업 상수(`app.js` 상단)와 같다.

```mermaid
stateDiagram-v2
  [*] --> Draft: 주제 선택
  Draft --> Collecting: 초대방 만들기<br/>마감 = 3h / 자정 / 24h(기본) / 48h
  note left of Collecting
    참가자 입장 · 답변 수집
    자동 리마인드 2회 (T/2, T-1h)
    수동 리마인드 (30분 쿨다운)
  end note
  Collecting --> Negotiating: ① 전원 답변 → 3초 뒤 자동<br/>② 방장 조기 시작 (미응답 조건 미반영 모달)<br/>③ 마감 도달 · 답변 ≥ 3명 → 자동 시작 배너
  Collecting --> Expired: 마감 도달 · 답변 < 3명
  Expired --> Collecting: 24h 연장<br/>(최대 2회)
  Expired --> Closed: 방 닫기
  Negotiating --> Proposed: 거부권 통과 후보 ≥ 1<br/>LLM 설명 30초 초과 시 규칙 문장 폴백
  Negotiating --> NoCandidate: 후보 0개
  NoCandidate --> Closed: 방 닫기 / 상한 완화 요청
  Proposed --> Ratifying: 재확인 시작 (마감 24h)
  Ratifying --> Ratifying: 동의 · 조건부 · 재조율 (익명 집계)
  Ratifying --> Negotiating: 재조율 요청 (방당 1회)
  Ratifying --> Confirmed: 방장 확정<br/>조건 = 재조율 0 ∧ (전원 응답 ∨ 24h 경과)
  Confirmed --> Closed: 7일 읽기 전용 후 원문 삭제 ※ 기간 미확정
  Closed --> [*]
```

---

## 4. 질문 6문항 세부 — 상한 토글과 구조화 확인

`questions` 화면 안의 흐름. 5·6번이 거부권을 만드는 자리라서 가장 세밀하다.

```mermaid
flowchart TD
  j["join · 이름 입력"] --> q1["1/6 방식<br/>실내 · 야외 · 반반"]
  q1 --> q2["2/6 시간<br/>반나절 · 하루 · 1박 2일<br/>☐ 이 이상은 어려워요 (거부권)"]
  q2 --> q3["3/6 목적 1·2순위<br/>팀워크 · 재충전 · 아이디어"]
  q3 --> q4["4/6 지역<br/>도심 · 근교 · 무관"]
  q4 --> q5["5/6 1인 예산<br/>3만 · 5만 · 10만 · 상관없음<br/>☐ 이 금액 넘으면 어려워요 (거부권)"]
  q5 --> q6["6/6 이것만은 피하고 싶어요<br/>자유서술 1줄 (선택)"]
  q6 --> parse{{"구조화<br/>(목업: 키워드 규칙 · 실제: LLM)"}}
  parse -->|"성공"| confirm["이렇게 이해했어요:<br/>운전 없는 동선 · 맞나요?"]
  parse -->|"실패"| fail["이 문장은 조건으로 바꾸지 못했어요<br/>다음 버튼 비활성"]
  fail -->|"다시 쓰기"| q6
  fail -->|"건너뛰기"| w
  confirm -->|"내 대리인 보내기"| w["waiting · 대기실"]
  q6 -->|"비워 두고 건너뛰기"| w
  q1 -.->|"← 이전"| j
  q2 -.->|"← 이전 (선택 유지)"| q1
  w -.->|"답변 고치기 (협상 시작 전)"| q1
  classDef veto fill:#fff7ed,stroke:#c2410c
  class q2,q5,q6,confirm veto
```

---

## 5. 초대 링크 발송 — 공용 링크 vs 개인 링크

방 만들기에서 고른 초대 방식에 따라 입장·리마인드·미응답 표시가 달라진다.

```mermaid
sequenceDiagram
  autonumber
  actor H as 방장
  participant S as 서버 (목업: localStorage)
  participant C as 초대 채널<br/>카톡 · 슬랙 · 문자
  actor P as 참가자

  H->>S: 초대방 만들기 (인원 N, 마감 T, 초대 방식)
  alt 공용 링크 + 입장 코드 (친구 · 가족 · 카톡)
    S-->>H: r/{room} + 6자리 코드
    H->>C: 링크 + 코드 한 번 공지
    C->>P: 링크 확인
    P->>S: 코드 입력 · 닉네임
    S-->>P: 입장 (정원 차면 닫힘)
    Note over S,P: 미입장자는 "참가자 N"으로만 표시<br/>같은 사람 재입장은 코드로 막지 못함
  else 이름별 개인 링크 (직장 팀 · 슬랙 · 메일)
    H->>S: 이름 N-1개 입력
    S-->>H: r/{room}/p/{token} 사람마다
    H->>P: 각자에게 개인 링크 전송
    P->>S: 토큰으로 입장 (중복 불가)
    Note over S,P: 방장은 "누가 미응답"까지 앎<br/>답변 내용은 여전히 못 봄<br/>토큰은 내 리포트 접근 인증에도 사용
  end
  P->>S: 6문항 답변 제출
  S-->>H: 참여 현황 갱신 (✓ 완료 · ● 작성 중 · ○ 미접속)
  loop 마감까지
    S->>C: 자동 리마인드 (T/2, T-1h) → 미응답자만
    H->>S: 수동 리마인드 (30분 쿨다운)
  end
  S-->>P: 링크 만료 = 답변 마감 시각
```

---

## 6. 답변 마감 타임라인

24시간 마감(기본)을 예로 든 시간축. 목업 시뮬레이터의 `+1h / +6h / 마감 1시간 전 / 마감` 버튼이 이 지점들을 밟는다.

```mermaid
flowchart TD
  t0["T0 09:00<br/>방 생성 · 링크 공유"] --> t12["T/2 21:00<br/>자동 리마인드 1<br/>미응답자만"]
  t12 --> t23["T-1h 08:00<br/>자동 리마인드 2"]
  t23 --> T{{"T 09:00 마감"}}
  T -->|"답변 ≥ 3"| auto["자동 협상 시작<br/>결과에 미응답 N명 배너"]
  T -->|"답변 < 3"| ext["expired 화면"]
  ext -->|"연장 1회 → T+24h"| t0b["리마인드 다시 계산<br/>(T'/2, T'-1h)"]
  t0b --> T2{{"T' 마감"}}
  T2 -->|"≥ 3"| auto
  T2 -->|"< 3 · 연장 2회 소진"| close["방 닫기"]
  t0 -.->|"방장 수동 알림<br/>언제든 · 30분 간격"| t12
  auto --> r["result → ratify<br/>재확인 마감 +24h"]
  r -->|"24h 경과"| abstain["미응답 = 기권 집계<br/>방장 확정 가능"]
  classDef t fill:#f5f5f4,stroke:#6b7280
  class t0,t12,t23,t0b t
```

---

## 7. 최종 재확인 — 추천이 아니라 합의로 끝내기

결과 화면은 "합의 제안"일 뿐이다. 확정은 전원 재확인 뒤 방장이 누른다.

```mermaid
flowchart TD
  r["result · 합의 제안"] --> c{"내 최종 의견"}
  c -->|"동의해요"| send["보내기"]
  c -->|"조건부로 동의해요<br/>공개 가능한 실행 조건 1줄"| send
  c -->|"다시 조율해야 해요<br/>새 조건 비공개 추가 · 요청자 익명"| send
  send --> tally["응답 현황 (이름 없이 합계)<br/>동의 · 조건부 · 재조율 · 미응답"]
  tally --> hasR{{"재조율 요청 ≥ 1?"}}
  hasR -->|"예 · 방당 1회"| nego["negotiating 재실행<br/>새 조건 합쳐 재계산"]
  nego --> r2["result · 재조율 1회 반영 배너<br/>이후 재조율 버튼 비활성"]
  r2 --> c
  hasR -->|"아니오"| allIn{{"전원 응답?"}}
  allIn -->|"예"| host{"방장: 결정 확정하기"}
  allIn -->|"아니오"| wait["재확인 마감까지 대기<br/>참가자 화면: 방장의 확정을 기다리는 중"]
  wait -->|"24h 경과"| abst["미응답 = 기권 집계 배너"]
  abst --> host
  host --> done["done · 확정 카드<br/>실행 조건 체크리스트 포함"]
  classDef veto fill:#fff7ed,stroke:#c2410c
  class nego,r2 veto
```

---

## 8. 예외 흐름 3종

검증 로그의 흐름 B·C에서 실제로 밟은 예외 경로.

```mermaid
flowchart LR
  subgraph E1["마감 미달 (shots 23)"]
    a1["마감 도달<br/>답변 2명"] --> a2["expired"] --> a3{"방장"}
    a3 -->|"24h 연장"| a4["waiting 복귀 · 연장 기록"]
    a3 -->|"방 닫기"| a5["home"]
  end
  subgraph E2["합의 불가 (shots 30)"]
    b1["반나절 상한 + 새벽 출발 금지"] --> b2["거부권 통과 후보 0개"] --> b3["noCandidate<br/>제외 사유는 주제만 표시"]
    b3 -->|"상한 완화 익명 동의 요청"| b4["상한 낸 사람 전원 동의 시 재계산 ※ 목업은 토스트"]
    b3 -->|"방 닫기"| b5["home"]
  end
  subgraph E3["LLM 타임아웃 (shots 29)"]
    c1["설명 생성 30초 초과"] --> c2["규칙 기반 문장으로 대체<br/>로그에 시스템 줄"] --> c3["result 배지<br/>설명은 간략하게 제공돼요"]
  end
```

---

## 9. 검증된 시나리오 경로

검증 로그(`VERIFY_LOG.md`)의 세 흐름을 화면 ID 열로 적은 것. 각 화살표는 위 다이어그램의 간선 하나에 대응한다.

| 흐름 | 조건 | 경로 |
|---|---|---|
| A 해피패스 | 4명 · 공용 링크 · 전원 답변 | home → setup → invite → join → questions(q0~q5, 구조화 확인) → waiting → negotiating(3초 자동) → result → receipt → ratify(동의 → 미응답 1 → 24h 마감 → 기권 집계) → done |
| B 타임아웃·재조율 | 8명 · 개인 링크 | home → setup → invite(리마인드 2회 + 수동 쿨다운) → join → questions → waiting → **expired**(답변 2명) → 연장 → waiting → negotiating(답변 4명 마감 자동 시작) → result(미응답 4명 배너) → ratify(재조율) → negotiating → result(재조율 1회 반영) → ratify(조건부 집계) → done |
| C 합의 불가 | 3명 · 반나절 상한 · 새벽 출발 금지 · LLM 타임아웃 토글 | home → setup → invite → join → questions → waiting → negotiating(폴백 시스템 줄) → **noCandidate** |

---

## 10. 다이어그램이 코드와 다른 곳 (미구현 · 가정)

- **개인 링크 전송**과 **공유 시트**는 목업에서 토스트만 뜬다. 시퀀스 다이어그램 §5의 전송 화살표는 실제 서비스 기준이다.
- **상한 완화 익명 동의**(§8 E2)는 목업에 버튼과 토스트만 있고 재계산은 없다.
- **7일 읽기 전용 후 원문 삭제**(§3)는 정책 미확정. 완료 화면 문구에만 있다.
- **자유서술 구조화**(§4)는 목업에서 키워드 7종 규칙이다. 실제로는 LLM이 맡고, 실패 처리 분기는 그대로 둔다.
- 참가자 화면에서 `ratify` 확정 버튼은 "방장의 확정을 기다리는 중"으로 비활성. 목업의 방장↔참가자 보기 전환으로만 확인했다.
