멈춘 작업 목록 패널 — 이어하기/완료 버튼 (MAMORU)

Project Identity: High-End Scissor Consulting Platform Tech Stack: Imweb(Frontend) + GAS doPost(e)(수신/기록) + Google Sheets(DB) + AppSheet(후속 관리/자동화)
CLAUDE.md 및 .claude/ADDENDUM_* 입니다..claude/ADDENDUM_IMWEB.md 기준탐색 → 정렬 → 안심 → 행동 준비 → 망설임 제거.claude/BRAND_COLOR_SYSTEM.md)#F2F2EA#181725#D4613E (CTA, 배지, 강조).mm- 접두사를 사용합니다.이동 시간 + 상담 시간 기준으로 전후 일정 차단(후속 관리는 AppSheet 우선).projects/as/_gas/| 방식 | 설명 |
|---|---|
| 방문수거 | 롯데택배 기사님이 고객 주소지로 방문하여 수거 |
| 직접발송 | 고객이 직접 택배 발송 (수거비 무료) |
| 항목 | 단가 |
|---|---|
| 마모루 가위 A/S | 10,000원/자루 |
| 타사 가위 A/S | 20,000원/자루 (마모루 가위 접수 시에만 추가 가능) |
| 총 수량 | 수거비 |
|---|---|
| 1자루 | 5,000원 |
| 2자루 | 3,000원 |
| 3자루 이상 | 무료 |
| 직접발송 | 무료 |
총 수량 = 마모루 가위 + 타사 가위 합산
{
"as_id": "AS-20260203-001",
"name": "고객명",
"phone": "01012345678",
"proceed_type": "방문수거|직접발송",
"pickup_date": "2026-02-05",
"qty_mamoru": "2",
"qty_other": "1",
"service_cost": 40000,
"shipping_fee": 0,
"total_amount": 40000
}
projects/
├── common_code/ # 아임웹 공통 코드 (환경설정)
│ ├── header_code_top.txt ← Header Code 상단 (preconnect)
│ ├── header_code.txt ← Header Code (iframe CSS)
│ ├── header_product_style.txt ← Header Code 추가 (상품 진열 공통 스타일)
│ └── footer_code.txt ← Footer Code (iframe JS)
├── product_page/ # 상품 진열 / 카테고리 페이지
│ └── productpage_categorybar.txt ← 카테고리 TOP 코드위젯 (배너+탭)
├── as/ # A/S 접수
│ ├── page_form.html ← GitHub Pages 배포 (실제 콘텐츠)
│ ├── page_guide.html ← GitHub Pages 배포
│ ├── iframe_form.html ← 아임웹 코드위젯에 붙여넣기 (/53)
│ ├── iframe_guide.html ← 아임웹 코드위젯에 붙여넣기 (/60)
│ ├── icons/ # SVG 아이콘
│ ├── holidays_2026_2027.tsv # 공휴일 원본 데이터
│ └── _gas/ # GAS 백엔드 (Code.gs 등)
├── consulting/ # 상담접수
│ ├── page_form.html ← GitHub Pages 배포
│ ├── page_diag.html ← GitHub Pages 배포
│ ├── page_guide.html ← GitHub Pages 배포
│ ├── page_recommend.html ← GitHub Pages 배포
│ ├── iframe_form.html ← 아임웹 코드위젯
│ ├── iframe_diag.html ← 아임웹 코드위젯
│ ├── iframe_guide.html ← 아임웹 코드위젯 (/61)
│ ├── iframe_recommend.html ← 아임웹 코드위젯
│ ├── products.js # 제품 데이터 (추천 로직)
│ └── Code.gs # GAS 백엔드
└── brand/ # 브랜드 소개
├── page_intro.html ← GitHub Pages 배포
├── iframe_intro.html ← 아임웹 코드위젯
└── Code.gs # GAS 백엔드
| 계층 | 파일 | 배포 방식 | 변경 시 반영 |
|---|---|---|---|
| 공통 코드 | common_code/*.txt | 아임웹 환경설정에 수동 붙여넣기 | 아임웹 환경설정에서 직접 교체 |
| 콘텐츠 | page_*.html | GitHub Pages 자동 배포 | git push → 1~2분 후 자동 |
| 코드위젯 | iframe_*.html, productpage_*.txt | 아임웹 코드위젯에 수동 붙여넣기 | 아임웹에서 직접 교체 필요 |
| 백엔드 | Code.gs | GAS 에디터에서 배포 | GAS 새 배포 필요 |
중요:
page_*.html수정은 push만 하면 되지만,iframe_*.html수정 시에는 아임웹 해당 페이지의 코드위젯도 동일 내용으로 교체해야 반영됩니다.
| 아임웹 환경설정 영역 | 로컬 파일 | 역할 |
|---|---|---|
| Header Code 상단 | common_code/header_code_top.txt | preconnect/dns-prefetch |
| Header Code | common_code/header_code.txt + header_product_style.txt | iframe CSS + 상품 진열 공통 스타일 |
| Footer Code | common_code/footer_code.txt | iframe 높이/모달 뷰포트 JS |
상품 진열 스타일은 Header Code에 한 번만 넣으면 전 카테고리 페이지에 적용됩니다. 새 상품 위젯 추가 시
header_product_style.txt의IDS배열에 위젯 ID만 추가하면 됩니다.
| 아임웹 경로 | 코드위젯 파일 | 로드하는 콘텐츠 |
|---|---|---|
/53 | projects/as/iframe_form.html | AS 접수 폼 |
/60 | projects/as/iframe_guide.html | AS 안내 |
/61 | projects/consulting/iframe_guide.html | 상담 안내 |
| (상담접수) | projects/consulting/iframe_form.html | 상담접수 폼 |
| (간편진단) | projects/consulting/iframe_diag.html | 간편진단 |
| (맞춤추천) | projects/consulting/iframe_recommend.html | 맞춤 추천 |
| (브랜드) | projects/brand/iframe_intro.html | 브랜드 소개 |
각 카테고리 페이지(블런트/틴닝/장가위/슬라이싱)는 코드위젯 2개로 구성:
| 순서 | 코드위젯 | 파일 | 역할 |
|---|---|---|---|
| 1 | 카테고리 TOP | productpage_categorybar.txt | 배너 이미지(PC/모바일 반응형) + 텍스트 오버레이 + 블랙 칩 탭 |
| 2 | 상품 진열 | 아임웹 기본 상품 위젯 | 스타일은 공통 코드(header_product_style.txt)에서 일괄 적용 |
카테고리 TOP 수정 포인트 (productpage_categorybar.txt):
<source srcset="..."> / 
— 배너 이미지 URL (PC/모바일).cat-banner-en — 영문 카테고리명 (예: BLUNT).cat-banner-kr — 한글 설명 (예: 커트 시작의 출발점).cat-mini a.active — 현재 페이지에 해당하는 탭에 active 클래스카테고리 페이지 매핑:
| 아임웹 경로 | 카테고리 | 상품 위젯 ID |
|---|---|---|
/29 | 블런트 | w2025082780aa858e21dc6 |
/30 | 틴닝 | (위젯 ID 확인 후 IDS 배열에 추가) |
/39 | 장가위 | (위젯 ID 확인 후 IDS 배열에 추가) |
/40 | 슬라이싱 | (위젯 ID 확인 후 IDS 배열에 추가) |
모든 page_*.html ↔ iframe_*.html 통신에 사용되는 표준 메시지 타입:
| 메시지 type | 방향 | 용도 |
|---|---|---|
MAMORU_IFRAME_SIZE | page → parent | iframe 높이 자동 조절 ({ height }) |
MAMORU_SCROLL_TOP | page → parent | 부모 페이지 맨 위로 스크롤 |
MAMORU_NAVIGATE | page → parent | 부모 페이지 이동 ({ url }) |
MAMORU_REQUEST_VIEWPORT | page → parent | 모달 중앙 배치용 뷰포트 정보 요청 |
MAMORU_VIEWPORT_INFO | parent → page | 뷰포트 정보 응답 ({ visibleTop, visibleHeight }) |
MAMORU_REQUEST_VIEWPORT/MAMORU_VIEWPORT_INFO는 iframe 내부에서 모달/달력을 현재 보이는 화면 중앙에 배치하기 위해 사용합니다. 모달이 있는 page에서 요청하면, iframe_*.html(부모)이 응답합니다.
7aa062f)<picture> PC/모바일 반응형 (6a8cb1e)73fb61d)IDS)로 일괄 관리, 페이지별 스타일 코드위젯 불필요 (22eb8c2)projects/common_code/ 폴더 신설 (83c4a2d)3324f94)openCalendar() 중복 호출 가드 (06488fa)removeAttribute('style') 타이밍 개선 (52b084a)iframe_form.html에 MAMORU_REQUEST_VIEWPORT 핸들러 추가 — iframe 내 모달 중앙 배치 지원 (52b084a) > 아임웹 /53 코드위젯도 동일 내용으로 교체 필요CLAUDE.md 및 .claude/ADDENDUM_*를 따릅니다.docs/reference/BRAND_GUIDE_FULL.mdhooks/register.tsx 209 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { ResumeItem } from '../types'
5import { countMalformed, kst, markDone, markUndone, parse } from './parse'
6
7// 멈춘 작업 목록 — 구글 드라이브 폴더라 PC 2대가 자동 동기화된다 (git 불필요)
8const DIR = 'C:/마모루드라이브/ClaudeWork'
9const FILE = `${DIR}/작업이어하기.md`
10const PANE = 'work-resume'
11
12const items = atom({ plugin: 'work-resume', key: 'items' } as const, [] as ResumeItem[])
13const error = atom({ plugin: 'work-resume', key: 'error' } as const, '')
14const warn = atom({ plugin: 'work-resume', key: 'warn' } as const, '')
15const checkedAt = atom({ plugin: 'work-resume', key: 'checkedAt' } as const, '')
16const sent = atom({ plugin: 'work-resume', key: 'sent' } as const, '')
17const confirm = atom({ plugin: 'work-resume', key: 'confirm' } as const, '')
18
19const TEMPLATE = `# 작업 이어하기
20
21> 하다가 멈춘 작업 목록. 클로드 코드 패널(work-resume mod)이 이 파일을 읽어 보여준다.
22> 형식: "## [진행중] 제목" 또는 "## [완료] 제목" + 아래 "- 항목: 값" 줄. 새 작업은 이 설명 바로 아래(맨 위)에 추가.
23
24`
25
26const PAUSE_WORDS = /(멈추자|멈출게|멈춰\s*줘|멈춤|중단하자|그만하자|여기까지\s*하자|나중에\s*이어)/
27const RESUME_WORDS = /(이어서|이어하|이어 하|하던\s*(거|것|작업))/
28
29const pauseGuide = () => `[작업 이어하기 규칙 — work-resume mod]
30사장님 말이 "지금 작업을 여기서 멈추자"는 뜻일 때만 따른다(아니면 무시).
31${FILE} 파일(없으면 만든다)의 설명 줄 바로 아래, 기존 항목들보다 위에 지금 작업을 아래 형식으로 추가한다. 기존 항목은 건드리지 않는다.
32
33## [진행중] <작업 제목 — 15자 안팎, 한국어>
34- id: <YYYYMMDD-HHMM, 지금 KST>
35- 멈춘 시각: <YYYY-MM-DD HH:MM KST>
36- 다음: <바로 다음에 할 일 한 줄>
37- 끝난 것: <한두 줄>
38- 남은 것: <한두 줄>
39- 관련 파일·커밋: <경로, 커밋 sha>
40- 주의: <승인 대기·검증 못 한 것 등, 없으면 생략>
41
42저장한 뒤 "작업 목록에 저장했습니다" 한 줄 + 미커밋 변경이 있으면 커밋할지 묻는다.`
43
44const resumeGuide = () => `[작업 이어하기 목록 — work-resume mod]
45하다가 멈춘 작업 기록 = ${FILE}. 사장님이 예전에 멈춘 작업을 이어가자는 뜻이면, 이 파일에서 해당 [진행중] 항목을 찾아 끝난 것·남은 것·주의를 읽고 이어서 진행한다. 이어간 작업이 끝나면 그 항목 머리줄을 [완료]로 바꾸고 "- 완료 시각:" 줄을 덧붙인다.`
46
47// 파일을 읽어 목록을 새로 그린다. 없으면 빈 틀을 만든다. 형식 오류·드라이브 충돌 사본도 함께 점검.
48async function load($: EngineInterface) {
49 try {
50 const has = await $.fs.exists(FILE)
51 if (!has) await $.fs.write(FILE, TEMPLATE)
52 const text = has ? ((await $.fs.read(FILE)) as string) : TEMPLATE
53 const notes: string[] = []
54 const bad = countMalformed(text)
55 if (bad > 0) notes.push(`형식이 안 맞는 항목 ${bad}개는 목록에 안 보입니다("## [진행중] 제목" 형식)`)
56 // 구글 드라이브가 PC 2대 동시 수정 때 만드는 충돌 사본(예: 작업이어하기 (1).md)
57 const copies = (await $.fs.list(DIR).catch(() => []))
58 .map(f => f.name)
59 .filter(n => /^작업이어하기.+\.md$/.test(n) && n !== '작업이어하기.md')
60 if (copies.length > 0) notes.push(`드라이브 충돌 사본이 있습니다: ${copies.join(', ')} — 클로드에게 합쳐 달라고 하세요`)
61 await update($, items, () => parse(text))
62 await update($, warn, () => notes.join(' / '))
63 await update($, error, () => '')
64 await update($, checkedAt, () => kst(Date.now()).slice(11))
65 } catch (err) {
66 await update($, error, () => `작업 목록 파일을 읽지 못했습니다: ${String(err).slice(0, 160)}`)
67 }
68}
69
70export const register: Register = on => {
71 on('session.start', async ($, e, next) => {
72 await $.command.register({ name: 'pause', description: '지금 작업을 "이어할 작업" 목록에 저장하고 멈춤' })
73 await $.command.register({ name: 'resume-list', description: '이어할 작업 패널 열기' })
74 await load($)
75 void $.ui.open({ id: PANE, title: '이어할 작업' })
76 // 다른 PC에서 드라이브로 넘어온 변경도 보이게 1분마다 다시 읽음
77 $.clock.every(60_000, () => void load($))
78 return next(e)
79 })
80
81 on('command.run', { command: 'pause' }, async $ => {
82 void $.prompt.submit({ text: `여기서 멈추자. 지금 작업을 이어할 작업 목록에 저장해줘.\n\n${pauseGuide()}` })
83 return { text: '지금 작업을 목록에 저장하도록 요청했습니다.' }
84 })
85
86 on('command.run', { command: 'resume-list' }, async $ => {
87 await load($)
88 await $.ui.open({ id: PANE, title: '이어할 작업' })
89 return { text: '이어할 작업 패널을 열었습니다.' }
90 })
91
92 // "여기서 멈추자" / "그거 이어서 하자" 같은 말에 규칙을 몰래 붙여 클로드에게 전달
93 on('prompt.submit', async ($, e, next) => {
94 if (e.origin?.kind === 'plugin') return next(e)
95 const add: string[] = []
96 if (PAUSE_WORDS.test(e.text)) add.push(pauseGuide())
97 if (RESUME_WORDS.test(e.text)) add.push(resumeGuide())
98 if (add.length === 0) return next(e)
99 return next({ ...e, context: [...(e.context ?? []), ...add] })
100 })
101
102 // 클로드가 파일을 고쳤을 수 있으니 턴이 끝날 때마다 다시 읽음
103 on('turn.complete', async ($, e, next) => {
104 const done = await next(e)
105 await load($)
106 return done
107 })
108
109 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
110 const { Box, Text, Button } = $.ui.resolve(e)
111 const list = await read($, items)
112 const problem = await read($, error)
113 const note = await read($, warn)
114 const at = await read($, checkedAt)
115 const sentKey = await read($, sent)
116 const confirmKey = await read($, confirm)
117 // 최근에 멈춘 작업이 위로
118 const open = list.filter(one => one.status === '진행중').sort((a, b) => b.when.localeCompare(a.when))
119 const done = list.filter(one => one.status === '완료').sort((a, b) => b.doneAt.localeCompare(a.doneAt))
120
121 // 이어하기: 클로드에게 "이 항목 기록 읽고 이어서" 요청을 보낸다. 5초 안 재클릭은 무시(중복 전송 방지)
122 const resume = (one: ResumeItem) => async () => {
123 if ((await read($, sent)) === one.key) return
124 await update($, sent, () => one.key)
125 $.clock.after(5000, () => void update($, sent, cur => (cur === one.key ? '' : cur)))
126 await $.prompt.submit({
127 text: `작업 이어하기: "${one.title}" (id ${one.key}). ${FILE} 에서 이 항목 기록(끝난 것·남은 것·주의)을 읽고 이어서 진행해줘. 진행하면서 큰 단계가 끝날 때마다 이 항목의 "다음·끝난 것·남은 것"을 갱신해줘.`,
128 })
129 }
130 // 완료: 실수 방지로 두 번 눌러야 완료(5초 안). 되돌리기는 아래 "최근 완료"에서
131 const finish = (one: ResumeItem) => async () => {
132 if ((await read($, confirm)) !== one.key) {
133 await update($, confirm, () => one.key)
134 $.clock.after(5000, () => void update($, confirm, cur => (cur === one.key ? '' : cur)))
135 return
136 }
137 await update($, confirm, () => '')
138 try {
139 const text = (await $.fs.read(FILE)) as string
140 await $.fs.write(FILE, markDone(text, one.key, kst(Date.now())))
141 $.ui.toast(`완료 처리: ${one.title}`)
142 } catch (err) {
143 $.ui.toast(`완료 처리 실패: ${String(err).slice(0, 120)}`)
144 }
145 await load($)
146 }
147 const undo = (one: ResumeItem) => async () => {
148 try {
149 const text = (await $.fs.read(FILE)) as string
150 await $.fs.write(FILE, markUndone(text, one.key))
151 $.ui.toast(`다시 진행중으로: ${one.title}`)
152 } catch (err) {
153 $.ui.toast(`되돌리기 실패: ${String(err).slice(0, 120)}`)
154 }
155 await load($)
156 }
157
158 return (
159 <Box flexDirection="column">
160 <Text dimColor>
161 진행중 {open.length}건{at !== '' ? ` · 마지막 확인 ${at}` : ''}
162 </Text>
163 {problem !== '' && <Text>⚠ {problem}</Text>}
164 {note !== '' && <Text>⚠ {note}</Text>}
165 {problem === '' && open.length === 0 && (
166 <Text dimColor>멈춘 작업이 없습니다. 작업 중에 "여기서 멈추자" 또는 /pause 라고 하면 여기에 쌓입니다.</Text>
167 )}
168 {open.map((one, i) => (
169 <Box key={one.key} flexDirection="column" marginTop={1}>
170 <Text bold>
171 {i + 1}. {one.title}
172 </Text>
173 {one.when !== '' && <Text dimColor>멈춘 시각 {one.when}</Text>}
174 {one.next !== '' && <Text>다음: {one.next}</Text>}
175 <Box flexDirection="row">
176 <Button
177 key={`resume-${one.key}`}
178 label={sentKey === one.key ? '보냈습니다 ✓' : '이어하기'}
179 variant="primary"
180 onPress={resume(one)}
181 />
182 <Button
183 key={`done-${one.key}`}
184 label={confirmKey === one.key ? '한 번 더 누르면 완료' : '완료'}
185 dimColor={confirmKey !== one.key}
186 onPress={finish(one)}
187 />
188 </Box>
189 </Box>
190 ))}
191 {done.length > 0 && (
192 <Box flexDirection="column" marginTop={1}>
193 <Text dimColor>최근 완료 (잘못 눌렀으면 되돌리기)</Text>
194 {done.slice(0, 3).map(one => (
195 <Box key={`d-${one.key}`} flexDirection="row">
196 <Text dimColor>
197 {one.title}
198 {one.doneAt !== '' ? ` · ${one.doneAt}` : ''}
199 </Text>
200 <Button key={`undo-${one.key}`} label="되돌리기" dimColor onPress={undo(one)} />
201 </Box>
202 ))}
203 </Box>
204 )}
205 </Box>
206 )
207 })
208}
209hooks/parse.ts 81 lines1import type { ResumeItem } from '../types'
2
3// KST 시각 문자열 (YYYY-MM-DD HH:MM)
4export const kst = (ms: number) => new Date(ms + 9 * 3600 * 1000).toISOString().slice(0, 16).replace('T', ' ')
5
6const HEAD = /^##\s*\[(진행중|완료)\]\s*(.+?)\s*$/
7
8export function parse(text: string): ResumeItem[] {
9 const out: ResumeItem[] = []
10 let cur: ResumeItem | undefined
11 for (const line of text.split(/\r?\n/)) {
12 const head = line.match(HEAD)
13 if (head) {
14 cur = { key: '', status: head[1] === '완료' ? '완료' : '진행중', title: head[2] ?? '', when: '', next: '', doneAt: '' }
15 out.push(cur)
16 continue
17 }
18 if (/^##\s/.test(line)) {
19 cur = undefined // 형식이 안 맞는 머리줄 — 아래 필드를 엉뚱한 항목에 붙이지 않는다
20 continue
21 }
22 if (!cur) continue
23 const field = line.match(/^-\s*(id|멈춘 시각|다음|완료 시각)\s*:\s*(.*?)\s*$/)
24 if (!field) continue
25 const value = field[2] ?? ''
26 if (field[1] === 'id') cur.key = value
27 else if (field[1] === '멈춘 시각') cur.when = value
28 else if (field[1] === '완료 시각') cur.doneAt = value
29 else cur.next = value
30 }
31 return out.map(one => ({ ...one, key: one.key || one.title }))
32}
33
34/** "## " 로 시작하지만 [진행중]/[완료] 형식이 아닌 머리줄 수 — 패널에 경고로 띄운다 */
35export function countMalformed(text: string): number {
36 return text.split(/\r?\n/).filter(line => /^##\s/.test(line) && !HEAD.test(line)).length
37}
38
39/** key(id 또는 제목)로 지정 상태의 항목 머리줄 위치를 찾는다 */
40function findHead(lines: string[], key: string, status: '진행중' | '완료'): number {
41 const want = new RegExp(`^##\\s*\\[${status}\\]\\s*(.+?)\\s*$`)
42 let block = -1
43 for (let i = 0; i < lines.length; i++) {
44 const line = lines[i] ?? ''
45 if (/^##\s/.test(line)) {
46 const head = line.match(want)
47 block = head ? i : -1
48 if (head && head[1] === key) return i // id 없는 항목은 제목이 key
49 continue
50 }
51 const id = line.match(/^-\s*id\s*:\s*(.*?)\s*$/)
52 if (block >= 0 && id && id[1] === key) return block
53 }
54 return -1
55}
56
57// 해당 항목의 머리줄을 [완료]로 바꾸고 완료 시각을 남긴다 (기록은 지우지 않음 — 이력 보존)
58export function markDone(text: string, key: string, at: string): string {
59 const lines = text.split(/\r?\n/)
60 const headAt = findHead(lines, key, '진행중')
61 if (headAt < 0) return text
62 lines[headAt] = (lines[headAt] ?? '').replace('[진행중]', '[완료]')
63 lines.splice(headAt + 1, 0, `- 완료 시각: ${at}`)
64 return lines.join('\n')
65}
66
67// 잘못 누른 [완료] 되돌리기 — 머리줄을 [진행중]으로, 그 항목의 "완료 시각" 줄은 지운다
68export function markUndone(text: string, key: string): string {
69 const lines = text.split(/\r?\n/)
70 const headAt = findHead(lines, key, '완료')
71 if (headAt < 0) return text
72 lines[headAt] = (lines[headAt] ?? '').replace('[완료]', '[진행중]')
73 for (let i = headAt + 1; i < lines.length && !/^##\s/.test(lines[i] ?? ''); i++) {
74 if (/^-\s*완료 시각\s*:/.test(lines[i] ?? '')) {
75 lines.splice(i, 1)
76 break
77 }
78 }
79 return lines.join('\n')
80}
81types/index.d.ts 26 lines1export type ResumeItem = {
2 key: string
3 status: '진행중' | '완료'
4 title: string
5 when: string
6 next: string
7 doneAt: string
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'work-resume': {
13 items: ResumeItem[]
14 error: string
15 /** 형식 경고·드라이브 충돌 사본 안내 */
16 warn: string
17 /** 마지막으로 파일을 읽은 시각 (KST HH:MM) */
18 checkedAt: string
19 /** 이어하기를 막 누른 항목 key — 5초간 중복 전송 방지 */
20 sent: string
21 /** 완료 확인 대기 중인 항목 key — 한 번 더 눌러야 완료 */
22 confirm: string
23 }
24 }
25}
26