Jump to content
Main menu
Main menu
move to sidebar
hide
Navigation
Main page
Recent changes
Random page
freem
Search
Search
Appearance
Create account
Log in
Personal tools
Create account
Log in
Pages for logged out editors
learn more
Contributions
Talk
Editing
Openai/695d0a1a-1d10-8010-bbdc-1d5591edf3f3
(section)
Add languages
Page
Discussion
English
Read
Edit
Edit source
View history
Tools
Tools
move to sidebar
hide
Actions
Read
Edit
Edit source
View history
General
What links here
Related changes
Special pages
Page information
Appearance
move to sidebar
hide
Warning:
You are not logged in. Your IP address will be publicly visible if you make any edits. If you
log in
or
create an account
, your edits will be attributed to your username, along with other benefits.
Anti-spam check. Do
not
fill this in!
== gpt-5-2-thinking == === User: ์ญํ : ๋๋ ๋ง์ผํ CRM ๊ณ ๊ฐ ๋ถ์ 20๋ ์ฐจ ์ ๋ฌธ๊ฐ์ผ. === ์ญํ : ๋๋ ๋ง์ผํ CRM ๊ณ ๊ฐ ๋ถ์ 20๋ ์ฐจ ์ ๋ฌธ๊ฐ์ผ. ์์ /๋ชฉํ : ๊ณ ๊ฐ ํผ๋๋ฐฑ ์๋ฃ๋ฅผ ์ฌ๋ ค์ฃผ๋ฉด, ์คํ๋ผ์ธ, ์จ๋ผ์ธ(๋๋ฆฌ๋ฒ๋ฆฌ), ๊ณ ๊ฐ ํผ๋๋ฐฑ์ ๊ธ์ /๋ถ์ 5์ ๋ง์ ํ๊ฐ์ ์๋ก ๋งค์ฅ๋ณ ํ๊ฐ๋ฅผ ํด์ฃผ๊ณ , ํด์ผํ ์ผ๋ค์ ์ฐ์ ์์๋ก ์ ๋ฆฌํด์ค. ๋งฅ๋ฝ : ํ์ฌ๋ ๋ง์ผํ ์ง์์ด ํ๋ํ๋ ๊ณ ๊ฐ ๋๊ธ์ ์ฝ๊ณ ๋งค์ฅ๋ณ๋ก ๊ฐ์ ์ ์ ์์ ๋ก ์ ๋ฆฌํ๊ณ ์์ด์, ์๊ฐ๋ ์ค๋๊ฑธ๋ฆฌ๊ณ , ์ด๋ ๋งค์ฅ์ ์ด๋ค ๊ฒ์ ์ฐ์ ์ ์ผ๋ก ํด๊ฒฐํด์ผํ๋์ง ์ ๋ฆฌ๊ฐ ์ ์๋ผ. HTML ์์ค๋ก ๋ง๋ค์ด์ค. === Assistant: ์๋ HTML์ ์ ๋ก๋ํ์ ์ฃผ๊ฐ ๊ณ ๊ฐํผ๋๋ฐฑPDF ๊ธฐ์ค์ผ๋ก, ๋งค์ฅ๋ณ๋ก ์คํ๋ผ์ธGoogle / ์จ๋ผ์ธGrabยทFoodpanda ๋ฆฌ๋ทฐ๋ฅผ 5์ ๋ง์ ํ๊ท ์ ์๋ก ์ง๊ณํ๊ณ , ์ด์ ์ ํํธ๋์ธ์ดํํฐ/๋๊ธฐ/๋๋ฝ/์ฝ๋ํธ๋/ํจํค์ง/์โฆ === ์๋ HTML์ ์ ๋ก๋ํ์ ์ฃผ๊ฐ ๊ณ ๊ฐํผ๋๋ฐฑ(PDF) ๊ธฐ์ค์ผ๋ก, ๋งค์ฅ๋ณ๋ก ์คํ๋ผ์ธ(Google) / ์จ๋ผ์ธ(GrabยทFoodpanda) ๋ฆฌ๋ทฐ๋ฅผ 5์ ๋ง์ ํ๊ท ์ ์๋ก ์ง๊ณํ๊ณ , '''์ด์ ์ ํ(ํธ๋์ธ์ดํํฐ/๋๊ธฐ/๋๋ฝ/์ฝ๋ํธ๋/ํจํค์ง/์๋น์คํ๋/์์ ๋ฑ)'''์ ์๋ ํ๊น ํด์ ์ฐ์ ์์ ์ก์ ์ ๋ฝ์์ฃผ๋ โ1ํ์ด์ง ๋์๋ณด๋โ ํ ํ๋ฆฟ์ ๋๋ค. * ์ด๋ฒ ์ฃผ ๋ฐ์ดํฐ์์ ํธ๋ ์ธ์ดํํฐ(์ธ๋์ฟกยท์์ค๋ ยท์ ์ ์ธ๊ธ) ๊ฐ์ โCriticalโ ์ผ์ด์ค๊ฐ ์ค์ ๋ก ์กด์ฌํด, ํด๋น ๋งค์ฅ์ ์ ์์ ๋ฌด๊ดํ๊ฒ ์ต์์ ์ฐ์ ์์๋ก ๊ณ ์ ๋๋๋ก ์ค๊ณํ์ต๋๋ค. weekly_reviews_20251229_to_2026โฆ * ์จ๋ผ์ธ(๋๋ฆฌ๋ฒ๋ฆฌ) ์์ญ์์๋ ๋ฏธ๋ด์ธ/๋๋ฝ/์ง์ฐ/์ฝ๋ํธ๋๊ฐ ๋ฐ๋ณต์ ์ผ๋ก ๋ณด์ด๋ฉฐ(์: ๋ฒ๊ฑฐ ๋๋ฝ+๋ฏธ๋ด์ธ), ์จ๋ผ์ธ ์ด์ ํ๋ก์ธ์ค ์ ๊ฒ์ด ์ฐ์ ๊ณผ์ ๋ก ์กํ๋๋ก ๋์ด ์์ต๋๋ค. weekly_reviews_20251229_to_2026โฆ * ์คํ๋ผ์ธ์์๋ ๋๊ธฐ์๊ฐ(์: 1์๊ฐ, 35๋ถ, 40๋ถ) ๋ฐ ์๋ ํ๋/์์(์ฐ๋ ๊ธฐ ๋์) ์ ํ์ด ํ์ธ๋ฉ๋๋ค. weekly_reviews_20251229_to_2026โฆ weekly_reviews_20251229_to_2026โฆ * ์ผ๋ถ ๋งค์ฅ/ํ๋ซํผ์์ ์คํธ๋ก์ฐ/์ปคํ๋ฌ๋ฆฌ ๋ฑ ๊ธฐ๋ณธ ๊ตฌ์ฑํ ๋๋ฝ์ด ๋ฐ๋ณต ์ธ๊ธ๋ฉ๋๋ค. weekly_reviews_20251229_to_2026โฆ weekly_reviews_20251229_to_2026โฆ <syntaxhighlight lang="html"><!doctype html> <html lang="ko"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>Weekly Reviews Dashboard (Offline/Online) โ Outlet Scoring & Action Priority</title> <style> :root{ --bg:#0b0f14; --card:#111826; --muted:#8aa0b5; --text:#e8f0f7; --line:#1e2a3a; --chip:#0e2234; --warn:#ffcc66; --bad:#ff6b6b; --good:#66ffb3; --accent:#7dd3fc; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,"Apple SD Gothic Neo","Noto Sans KR","Malgun Gothic",sans-serif;} .wrap{max-width:1180px;margin:0 auto;padding:20px 16px 40px;} h1{font-size:20px;margin:0 0 8px;} .sub{color:var(--muted);font-size:13px;line-height:1.45;margin-bottom:16px;} .row{display:grid;grid-template-columns:1.2fr 0.8fr;gap:12px;align-items:start;} @media (max-width: 980px){ .row{grid-template-columns:1fr} } .card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:14px;} .controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center} .controls label{font-size:12px;color:var(--muted)} input[type="search"], select{ background:#0c1420;border:1px solid var(--line);color:var(--text); padding:10px 10px;border-radius:12px;outline:none;min-width:220px; } .toggle{ display:flex;gap:8px;align-items:center;padding:8px 10px;border:1px solid var(--line); border-radius:999px;background:#0c1420; } .toggle input{transform:scale(1.05)} .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px} @media (max-width: 980px){ .kpis{grid-template-columns:repeat(2,1fr)} } .kpi{background:#0c1420;border:1px solid var(--line);border-radius:14px;padding:12px;} .kpi .t{color:var(--muted);font-size:12px} .kpi .v{font-size:18px;margin-top:6px} .kpi .v small{font-size:12px;color:var(--muted)} table{width:100%;border-collapse:separate;border-spacing:0} thead th{ font-size:12px;color:var(--muted);text-align:left;padding:10px;border-bottom:1px solid var(--line); position:sticky;top:0;background:linear-gradient(#111826,#111826); z-index:2; } tbody td{padding:10px;border-bottom:1px solid var(--line);vertical-align:top;font-size:13px} tbody tr:hover{background:#0c1420} .pill{ display:inline-flex;gap:6px;align-items:center;padding:6px 10px;border-radius:999px; background:var(--chip);border:1px solid #15324b;color:var(--text);font-size:12px;white-space:nowrap; } .pill.critical{border-color:#5b1b1b;background:#1a0f12} .pill.critical b{color:var(--bad)} .score{font-variant-numeric:tabular-nums} .score.good{color:var(--good)} .score.bad{color:var(--bad)} .score.mid{color:var(--warn)} details{border:1px solid var(--line);border-radius:14px;background:#0c1420;margin-top:10px} details summary{cursor:pointer;padding:10px 12px;color:var(--accent);font-size:13px} .detail{padding:0 12px 12px;color:var(--text)} .chips{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0 10px} .chips .pill{background:#0a1a2a} .actions{margin:0;padding-left:18px;color:#d8e6f3} .actions li{margin:6px 0} .ex{border:1px solid var(--line);border-radius:12px;padding:10px;margin-top:10px;background:#091321} .ex .meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--muted);font-size:12px;margin-bottom:6px} .ex .msg{font-size:13px;line-height:1.45} .muted{color:var(--muted)} .right .card{position:sticky;top:12px} .foot{margin-top:12px;color:var(--muted);font-size:12px;line-height:1.5} .btn{ background:#0c1420;border:1px solid var(--line);color:var(--text);border-radius:12px; padding:9px 10px;cursor:pointer;font-size:12px; } .btn:hover{border-color:#27435f} .hr{height:1px;background:var(--line);margin:12px 0} code.inline{background:#0c1420;border:1px solid var(--line);padding:2px 6px;border-radius:8px;color:#d6f0ff} </style> </head> <body> <div class="wrap"> <h1>Weekly Reviews โ Outlet Scoring & Action Priority</h1> <div class="sub"> ์คํ๋ผ์ธ(Google) / ์จ๋ผ์ธ(GrabยทFoodpanda) ๋ฆฌ๋ทฐ๋ฅผ 5์ ๋ง์ ํ๊ท ์ผ๋ก ์ง๊ณํ๊ณ , ๋ฉ์์ง ํค์๋๋ก ์ด์๋ฅผ ์๋ ๋ถ๋ฅํด โ์ด๋ ๋งค์ฅ์ ๋ฌด์๋ถํฐโ ํด๊ฒฐํด์ผ ํ๋์ง ์ฐ์ ์์๋ก ๋ณด์ฌ์ค๋๋ค.<br/> <span class="muted">*Critical(ํธ๋ ์ธ์ดํํฐ ๋ฑ) ์ด์๊ฐ ์์ผ๋ฉด ์ ์์ ๋ฌด๊ดํ๊ฒ ์ต์๋จ ๊ณ ์ </span> </div> <div class="row"> <div class="left"> <div class="card"> <div class="controls"> <label>Search</label> <input id="q" type="search" placeholder="์: Suntec / Orchard / wait / missing / cold" /> <label>Sort</label> <select id="sort"> <option value="priority">Priority (Critical โ Highest)</option> <option value="offline">Offline score (low โ high)</option> <option value="online">Online score (low โ high)</option> <option value="overall">Overall score (low โ high)</option> <option value="volume">Review volume (high โ low)</option> </select> <div class="toggle"> <input id="onlyCritical" type="checkbox"/> <label for="onlyCritical">Critical only</label> </div> <div class="toggle"> <input id="negOnly" type="checkbox"/> <label for="negOnly">Show โneeds-fixโ first</label> </div> <button class="btn" id="reset">Reset</button> </div> <div class="hr"></div> <div class="kpis" id="kpis"></div> <div class="hr"></div> <div style="overflow:auto; max-height: 560px; border-radius:14px;"> <table> <thead> <tr> <th style="min-width:190px;">Outlet</th> <th>Flags</th> <th>Offline (Google)</th> <th>Online (Delivery)</th> <th>Overall</th> <th>Top Issues</th> <th style="min-width:120px;">Priority</th> </tr> </thead> <tbody id="rows"></tbody> </table> </div> <div class="foot"> <b>Scoring logic</b><br/> * Offline/Online Score: ํด๋น ์ฑ๋ ํ๊ท ๋ณ์ (1~5).<br/> * Priority: ์ด์ ์ ํ๋ณ ๊ฐ์ค์น ร (๋ถ๋ง๊ฐ๋=6-๋ณ์ ) ร ๋น๋ ๊ธฐ๋ฐ ์งํ.<br/> * Critical: โ์ธ๋์ฟก/์์ค๋ /์ ์/์ด๋ชจ๋ฌ๋ผ ๋ฑโ ํค์๋ ๊ฐ์ง ์ ์ต์ฐ์ ์ฒ๋ฆฌ. </div> </div> </div> <div class="right"> <div class="card"> <div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start;"> <div> <div class="muted" style="font-size:12px;">This week playbook</div> <div style="font-size:16px;margin-top:4px;"><b>Recommended workflow</b></div> </div> </div> <div class="hr"></div> <ol class="actions"> <li><b>Critical first:</b> Food safety/contamination/undercook์ 24์๊ฐ ๋ด CAPA(์์ธยท์กฐ์นยท์ฌ๋ฐ๋ฐฉ์ง) ์๋ฃ.</li> <li><b>Online ops:</b> Seal/packing checklist + item-scan + โmissing itemโ ์ฌ๋ฐ ๋งค์ฅ ์ง์ค ์ ๊ฒ.</li> <li><b>Speed & comms:</b> ํผํฌ ํ์ WIP ์ ํ + ์์ ๋๊ธฐ์๊ฐ ๊ณ ์ง + ํฝ์ ์กด ๋์ ์ ๋ฆฌ.</li> <li><b>Service basics:</b> ์๋ ํค/ํด๋ ์ ์ฒ๋ฆฌ ์คํฌ๋ฆฝํธ + ๋งค๋์ ์์ค์ปฌ๋ ์ด์ ๋ฃฐ.</li> <li><b>Close loop:</b> โNOT REPLIEDโ ๊ณ ์ํ ๊ฑด์ ์ฐ์ ์๋ต + ๋ณด์/์ฌ๋ฐ๋ฐฉ์ง ์๋ด ํ ํ๋ฆฟ ์ ์ฉ.</li> </ol> <div class="hr"></div> <div class="muted" style="font-size:12px;"> ํ์ ์, ๋ค์ ๋จ๊ณ๋ก โOwner/ETA/Statusโ ์นผ๋ผ๊น์ง ๋ถ์ฌ์ <code class="inline">Action Tracker</code> ํํ๋ก ํ์ฅ ๊ฐ๋ฅํฉ๋๋ค. </div> </div> </div> </div> </div> <script> /** * Data source: weekly_reviews_20251229_to_20260104.pdf * Offline = Google, Online = Grab/Foodpanda * Note: priority_score is a heuristic indicator; Critical flag overrides sorting. */ const DATA = ${json_data}; const ISSUE_TO_ACTIONS = { "Food safety / undercooked": [ "์ฆ์: ํด๋น SKU(์นํจ/ํจํฐ) ์กฐ๋ฆฌ์จ๋ยทํ์ด๋จธยทํ๋ฉํ์ ์ ์ ์ ๊ฒ (๋งค๋์ ์๋ช ).", "์์ธ: ํผํฌํ์ ์์ฐ๋/์คํํ/๊ทธ๋ฆด ๋ก๋ฉ/๋๊ป ํธ์ฐจ/์จ๋๊ณ ๊ต์ ์ฌ๋ถ ํ์ธ.", "์ฌ๋ฐ๋ฐฉ์ง: QA ์ฒดํฌ(์ฒซ 30๋ถยทํผํฌ ์ค 2ํ) + ๊ต์ก ๋ฆฌ๋ง์ธ๋ + ๊ฐ์ฌ(spot check) 2์ฃผ ์ด์." ], "Long wait / queue": [ "ํผํฌํ์ WIP ์ ํ(๋ฒ๊ฑฐ/ํ๋ผ์ด ๋์ ์ฒ๋ฆฌ ๊ธฐ์ค) + ๋ณ๋ชฉ ์คํ ์ด์ ์ฌ๋ฐฐ์น.", "์์ ๋๊ธฐ์๊ฐ ์๋ด(์นด์ดํฐ/ํฝ์ ์กด) + ํฝ์ ํธ์ถ ํ๋ก์ธ์ค ์ ๋ฆฌ(๋ฒํธ/์ด๋ฆ).", "์ธ๋ ฅ: ํผํฌ 2์๊ฐ ๊ตฌ๊ฐ(์ฃผ๋ง/ํ์ฌ) ์คํํ ๋ณด๊ฐ + ๋ฌ๋(ํฝ์ ์กด) ์ง์ ." ], "Missing item / condiments": [ "ํจํน ์ฒดํฌ๋ฆฌ์คํธ(2-step: packer + checker) ๋์ /๊ฐ์ .", "์์ค/์คํธ๋ก์ฐ/์ปคํ๋ฌ๋ฆฌ ๋ฑ โ๊ธฐ๋ณธ ๊ตฌ์ฑํโ์ ๋ฒ๋ค๋ก ๋ฌถ์ด ์๋ ํฌ์ .", "์ฌ๋ฐ ๋งค์ฅ์ POS ์์ดํ ์ค์บโ๋ฐฑ ํ์ธ(visual) ๋ฃจํด ๊ต์ก." ], "Cold food": [ "ํซ/์ฝ๋ ๋ถ๋ฆฌ ํจํน(์๋ฃ ์์ ํ๋ผ์ด/์นํจ ๊ธ์ง) + ๋จ์ด/ํ๊ธฐ ๊ธฐ์ค ์ ๊ฒ.", "๋ผ์ด๋ ํฝ์ ๋๊ธฐ ์ ๋ณด๊ด ์์น(์๋จธ/์ ๋ฐ) ํ์คํ.", "์ฅ์๊ฐ ์ง์ฐ ์ฃผ๋ฌธ์ ๋ฆฌ๋ฉ์ดํฌ ๋ฃฐ(์๊ณ์๊ฐ) ์ ์ฉ." ], "Packaging leak / spill": [ "์ปต/์์ค/์์ดํฌ ์ฐ๋ง ์์ฌ ์ ๊ฒ + ๋ดํฌ ์ ์ฌ ๋ฐฉ์ ๊ต์ก.", "์ก์ฒด๋ฅ๋ ๋ณ๋ ์บ๋ฆฌ์ด/์นธ๋ง์ด ๊ณ ์ , ์คํ์ปค๋ธ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝ.", "โ์คํโ ๋น๋ฐ ์๊ฐ๋/๋ฉ๋ด ์ถ์ ํด ํฌ์ฅ ์คํ ์์ ." ], "Service attitude / rude": [ "ํด๋ ์ ์๋ ์คํฌ๋ฆฝํธ(3-step: ์ฌ๊ณผโํ์ธโํด๊ฒฐ) ๊ต์ก + ๋งค๋์ ์์ค์ปฌ๋ ์ด์ ๋ฃฐ.", "ํผํฌํ์ ์คํธ๋ ์ค ๊ตฌ๊ฐ์ โํ๋ก ํธ ๋ฆฌ๋โ ๋ฐฐ์นํด ํค ๊ด๋ฆฌ.", "์ง์ ์นญ์ฐฌ ๊ฑด์ ์ฃผ๊ฐ Kudos๋ก ๊ณต์ ํด ๊ธ์ ํ๋ ๊ฐํ." ], "Cleanliness / smell / pests": [ "์ฐ๋ ๊ธฐ/ํธ๋ ์ด/ํฝ์ ์กด ์ฒญ์ ์ฃผ๊ธฐ ์ฌ์ ์(ํผํฌ 30๋ถ ๋จ์).", "๋ฐฑ์ค๋ธํ์ฐ์ค ํ๊ธฐ๋ฌผ ๋์ ยท๋ณด๊ดยท๋ฐฐ์ถ ์๊ฐ ์ ๊ฒ (๋์ ์์ธ ์ฐจ๋จ).", "ํด์ถฉ(์กฐ๋ฅ/๋ฒ๋ ) ์ ์ ํฌ์ธํธ(์ถ์ ๋ฌธ/์ธ๋ถ ์ข์) ์ฐจ๋จ ๋ฐ ์์ค ์ ๊ฒ." ], "App / order status": [ "โ์ค๋น์๋ฃโ/โํฝ์ ๊ฐ๋ฅโ ์ํ ์ ๋ฐ์ดํธ ํ์ด๋ฐ ํ์คํ.", "๋ผ์ด๋/๊ณ ๊ฐ ๋๊ธฐ ๋ฐ์ ์: ์์ ๋ด๋น์ ์ง์ + ์ปค๋ฎค๋์ผ์ด์ ๋ฌธ๊ตฌ ํต์ผ.", "์ทจ์/ํ๋ถ ์ด์๋ ํ๋ซํผ๋ณ SOP(์ฆ๋น/์๋ต ํ ํ๋ฆฟ) ์ ์ฉ." ], "Menu quality / taste": [ "ํน์ ์ฌ๋ฃ(ํผํด/์์ค/์ฐ๋ฏธ) ๋ถ๋ง์ ๋ ์ํผ ๋ฒ์ ๋ด ์กฐ์ (์ต์ ์๋ด ํฌํจ).", "ํ์ง ํธ์ฐจ(์ค๋ฒ/์ธ๋, ์๊ฐ) ์์ธ: ํ๋ฉยท๋ฐฐ์นยท๋ฆฌ๋ฉ์ดํฌ ๊ธฐ์ค ์ ๊ฒ." ] }; function fmtScore(v){ if (v === null || v === undefined || Number.isNaN(v)) return <code><span class="muted">n/a</span></code>; const cls = v >= 4.4 ? "good" : (v <= 3.6 ? "bad" : "mid"); return <code><span class="score ${cls}">${v.toFixed(2)}</span></code>; } function topIssuesHtml(issues){ if (!issues || !issues.length) return <code><span class="muted">-</span></code>; return issues.slice(0,3).map(x => <code><span class="pill">${x.tag} <span class="muted">(${x.count})</span></span></code>).join(" "); } function buildActions(topIssues){ const uniq = [...new Set((topIssues||[]).map(x=>x.tag))].slice(0,4); const lines = []; for (const tag of uniq){ const actions = ISSUE_TO_ACTIONS[tag]; if (actions && actions.length){ lines.push(<code><li><b>${tag}</b><ul class="actions">${actions.map(a=></code><li>${a}</li><code>).join("")}</ul></li></code>); } } if (!lines.length) return <code><div class="muted">ํน์ด ์ด์๊ฐ ์ ์ด, ํ์ค ์ด์ ์ ์ง + ์นญ์ฐฌ ๋ฆฌ๋ทฐ ์ค์ฌ์ผ๋ก ๋ก์ดํฐ ๊ฐํ ํ๋ ์ถ์ฒ</div></code>; return <code><ol class="actions">${lines.join("")}</ol></code>; } function examplesHtml(examples){ if (!examples || !examples.length) return <code><div class="muted">No sample comments</div></code>; return examples.slice(0,3).map(ex => { const tags = (ex.issue_tags || []).map(t=><code><span class="pill">${t}</span></code>).join(" "); return <code> <div class="ex"> <div class="meta"> <span class="pill">${ex.channel}</span> <span class="pill">${ex.platform}</span> <span class="pill">โ ${ex.rating}</span> <span class="muted">Ticket ${ex.ticket}</span> <span class="muted">${ex.created}</span> </div> <div class="chips">${tags || </code><span class="muted">-</span><code>}</div> <div class="msg">${escapeHtml(ex.message || "")}</div> </div> </code>; }).join(""); } function escapeHtml(s){ return (s||"") .replaceAll("&","&").replaceAll("<","<") .replaceAll(">",">").replaceAll('"',""").replaceAll("'","'"); } function calcKPIs(list){ const totalOutlets = list.length; const critical = list.filter(x=>x.critical_food_safety).length; const totalReviews = list.reduce((a,x)=>a + (x.total_n||0), 0); const avgOverall = totalOutlets ? (list.reduce((a,x)=>a + (x.overall_avg||0),0)/totalOutlets) : 0; return { totalOutlets, critical, totalReviews, avgOverall }; } function renderKPIs(list){ const {totalOutlets, critical, totalReviews, avgOverall} = calcKPIs(list); const el = document.getElementById("kpis"); el.innerHTML = <code> <div class="kpi"><div class="t">Outlets</div><div class="v">${totalOutlets}</div></div> <div class="kpi"><div class="t">Critical outlets</div><div class="v"><span style="color:var(--bad)">${critical}</span></div></div> <div class="kpi"><div class="t">Total reviews</div><div class="v">${totalReviews}</div></div> <div class="kpi"><div class="t">Avg overall score</div><div class="v">${avgOverall.toFixed(2)} <small>/ 5</small></div></div> </code>; } function sortData(list, mode){ const arr = [...list]; if (mode === "priority"){ // Critical first, then higher priority_score, then lower overall score arr.sort((a,b)=>{ if (!!a.critical_food_safety !== !!b.critical_food_safety) return (b.critical_food_safety?1:0) - (a.critical_food_safety?1:0); if ((b.priority_score||0) !== (a.priority_score||0)) return (b.priority_score||0) - (a.priority_score||0); return (a.overall_avg||0) - (b.overall_avg||0); }); } else if (mode === "offline"){ arr.sort((a,b)=>(a.offline_avg ?? 99) - (b.offline_avg ?? 99)); } else if (mode === "online"){ arr.sort((a,b)=>(a.online_avg ?? 99) - (b.online_avg ?? 99)); } else if (mode === "overall"){ arr.sort((a,b)=>(a.overall_avg ?? 99) - (b.overall_avg ?? 99)); } else if (mode === "volume"){ arr.sort((a,b)=>(b.total_n||0) - (a.total_n||0)); } return arr; } function needsFixFirst(list){ // heuristic: any avg <= 3.8 or any critical const bad = list.filter(x=>x.critical_food_safety || (x.overall_avg ?? 5) <= 3.8); const good = list.filter(x=>!(x.critical_food_safety || (x.overall_avg ?? 5) <= 3.8)); return [...bad, ...good]; } function render(){ const q = (document.getElementById("q").value || "").toLowerCase().trim(); const sortMode = document.getElementById("sort").value; const onlyCritical = document.getElementById("onlyCritical").checked; const negOnly = document.getElementById("negOnly").checked; let list = DATA.filter(x=>{ if (onlyCritical && !x.critical_food_safety) return false; if (!q) return true; const hay = [ x.outlet, ...(x.top_issues||[]).map(t=>t.tag), ...(x.examples||[]).map(e=>e.message) ].join(" ").toLowerCase(); return hay.includes(q); }); list = sortData(list, sortMode); if (negOnly) list = needsFixFirst(list); renderKPIs(list); const tbody = document.getElementById("rows"); tbody.innerHTML = list.map(x=>{ const flags = x.critical_food_safety ? <code><span class="pill critical"><b>CRITICAL</b> Food safety</span></code> : <code><span class="muted">-</span></code>; const issues = topIssuesHtml(x.top_issues); const priority = x.critical_food_safety ? <code><span class="pill critical"><b>Critical</b></span></code> : <code><span class="pill">Score <span class="score mid">${(x.priority_score||0).toFixed(1)}</span></span></code>; const detailId = <code>d_${x.outlet.replace(/\s+/g,'_')}</code>; return <code> <tr> <td><b>${escapeHtml(x.outlet)}</b><div class="muted">${x.total_n} reviews</div></td> <td>${flags}</td> <td>${fmtScore(x.offline_avg)} <span class="muted">(${x.offline_n})</span></td> <td>${fmtScore(x.online_avg)} <span class="muted">(${x.online_n})</span></td> <td>${fmtScore(x.overall_avg)} <span class="muted">(${x.total_n})</span></td> <td>${issues}</td> <td>${priority}</td> </tr> <tr> <td colspan="7" style="padding-top:0;border-bottom:1px solid var(--line);"> <details id="${detailId}"> <summary>Show details: action plan + sample comments</summary> <div class="detail"> <div class="chips"> ${(x.top_issues||[]).map(t=></code><span class="pill">${t.tag} <span class="muted">(${t.count})</span></span><code>).join("") || </code><span class="muted">No issue tags</span><code>} </div> <div style="margin-top:6px;"><b>Recommended actions (priority order)</b></div> ${buildActions(x.top_issues)} <div style="margin-top:12px;"><b>Sample comments (worst-first)</b></div> ${examplesHtml(x.examples)} </div> </details> </td> </tr> </code>; }).join(""); } document.getElementById("q").addEventListener("input", render); document.getElementById("sort").addEventListener("change", render); document.getElementById("onlyCritical").addEventListener("change", render); document.getElementById("negOnly").addEventListener("change", render); document.getElementById("reset").addEventListener("click", ()=>{ document.getElementById("q").value=""; document.getElementById("sort").value="priority"; document.getElementById("onlyCritical").checked=false; document.getElementById("negOnly").checked=false; render(); }); render(); </script> </body> </html> </syntaxhighlight> ์ํ์๋ฉด, ์ด HTML์ ๋ค์ ํํ๋ก ๋ โ์ค๋ฌด์ฉโ์ผ๋ก ์ ๊ทธ๋ ์ด๋ํด๋๋ฆด๊ฒ์(์ง๋ฌธ ์ ํ์ ๋ ๋ฉ๋๋ค. ์ ๋ ๋ฐ๋ก ์์ ๊ฐ๋ฅํฉ๋๋ค). * Owner / ETA / Status(Open/In progress/Done)๊น์ง ๋ถ์ฌ์ โ์ฃผ๊ฐ Action Trackerโ๋ก ํ์ฅ * ๋งค์ฅ๋ณ๋ก Offline vs Online ๊ฐญ(ฮ) ์๋ ํ์ + โ์์ธ ๊ฐ์คโ๊น์ง ์ถ์ฒ * โ์ง์ ์ด๋ฆ ์ธ๊ธ(์นญ์ฐฌ ์ง์ ๋ฆฌํฌํธ)โ ์๋ ์ถ์ถ(์: One Fullerton์์ ํน์ ์ง์ ์นญ์ฐฌ์ฒ๋ผ) weekly_reviews_20251229_to_2026โฆ
Summary:
Please note that all contributions to freem are considered to be released under the Creative Commons Attribution-ShareAlike 4.0 (see
Freem:Copyrights
for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)