플러그인 없이 2회차 · 요청 3: 요청 2 + "핵심 부분은 직접 짜 보고 싶어" · Claude Code · Opus 5.5 · 생각 깊이 중간 · 목록

플러그인 없이 2회차

요청 3: 요청 2 + "핵심 부분은 직접 짜 보고 싶어"

요청문: src/stock.mjs에 todayTasks(items, today) 함수를 만들어 줘. 아침에 카페 사장님이 보는 '오늘 할 일' 목록이야. 유통기한이 지난 것, 3일 안에 끝나는 것, 수량이 minQty보다 적은 것을 한 목록으로 합쳐서 급한 것부터 보여 주고 싶어. today는 'YYYY-MM-DD' 문자열이야. test/stock.test.mjs에 테스트도 추가하고 돌려 봐. 나는 코딩을 배우는 중이라 핵심 부분은 직접 짜 보고 싶어.

클로드의 마지막 답변 (그대로)

`todayTasks`의 뼈대와 테스트를 만들어 두었고, 테스트도 돌려 봤습니다. 핵심인 "무엇이 더 급한가" 규칙은 직접 짜실 수 있도록 비워 두었어요. ## 테스트 결과 ``` 기본 테스트 통과 todayTasks 수집 테스트 통과 AssertionError: 유통기한 지난 것이 맨 앞 '우유' !== '딸기' ``` - **기존 테스트와 수집 테스트는 통과합니다.** 어떤 재료가 목록에 들어가는지, 이유가 제대로 붙는지, 이유가 두 개인 재료가 한 번만 나오는지를 확인합니다. - **순서 테스트는 실패합니다.** 아직 정렬 규칙이 없어서 입력 순서 그대로 나오기 때문이에요. 규칙을 채우면 통과하게 됩니다. ## 만들어 둔 부분 (`src/stock.mjs`) - `todayTasks(items, today)`는 재료마다 남은 날(`daysLeft`)을 계산하고, 해당하는 이유를 `reasons` 배열에 담습니다. - `daysLeft < 0`이면 `'expired'`(유통기한 지남) - `0 ≤ daysLeft ≤ 3`이면 `'expiringSoon'`(곧 만료). 오늘 끝나는 것과 딱 3일 남은 것도 여기에 들어갑니다. - `qty < minQty`이면 `'lowStock'`(재고 부족) - 우유처럼 두 이유에 걸리면 항목은 하나만 만들고 `reasons: ['expiringSoon', 'lowStock']`처럼 이유를 둘 다 담습니다. 같은 재료가 목록에 두 번 나오면 사장님이 헷갈릴 것 같아서 이렇게 했어요. - 마지막에 `tasks.sort(compareUrgency)`로 정렬합니다. ## 직접 짜 보실 부분 **1. `compareUrgency(a, b)`** (지금은 `return 0`만 있습니다) `sort`에 넘기는 비교 함수예요. a가 더 급하면 음수, b가 더 급하면 양수, 같으면 0을 돌려주면 됩니다. 테스트가 이미 정해 둔 규칙은 두 가지입니다. - 유통기한이 지난 것이 맨 앞에 옵니다. - 곧 만료되는 것끼리는 남은 날이 적은 쪽이 먼저입니다. 나머지 기준은 직접 정하셔야 하는 부분이고, 여기가 이 함수의 핵심입니다. - 2일 남은 재료와 재고만 부족한 재료(예: 종이컵) 중 어느 쪽이 먼저일까요? - 이유가 두 개 겹친 재료(우유, 치즈케이크)는 더 앞으로 보낼까요? - 재고만 부족한 재료끼리는 무엇으로 줄을 세울까요? 모자란 양, 비율, 이름 중에서 고를 수 있어요. 팁: 각 항목에 "급한 정도 점수"를 매겨 주는 작은 함수를 따로 만들면, `return score(a) - score(b)` 한 줄로 비교 함수를 끝낼 수 있습니다. **2. `test/stock.test.mjs` 맨 아래의 TODO** 위에서 정한 규칙을 테스트로도 적어 보세요. 예를 들어 `assert.ok(before('우유', '종이컵'))`처럼 쓰면 됩니다. 다 짠 다음 `node test/stock.test.mjs`를 실행해서 마지막 줄에 `todayTasks 순서 테스트 통과`가 나오면 끝이에요. 코드를 보여 주시면 리뷰해 드릴게요.

바뀐 코드 (git diff)

diff --git a/src/stock.mjs b/src/stock.mjs
index f81b897..2934e4d 100644
--- a/src/stock.mjs
+++ b/src/stock.mjs
@@ -19,3 +19,27 @@ export function orderList(items) {
     .filter((it) => it.qty < it.minQty)
     .map((it) => ({ name: it.name, need: it.minQty * 2 - it.qty, unit: it.unit }));
 }
+
+// 아침에 보는 '오늘 할 일' 목록
+// task: { name, reasons: ['expired' | 'expiringSoon' | 'lowStock', ...], daysLeft, qty, minQty, unit }
+// 한 재료가 여러 이유에 걸리면 task 하나에 reasons를 모두 담는다.
+export function todayTasks(items, today) {
+  const tasks = [];
+  for (const it of items) {
+    const daysLeft = daysBetween(today, it.expires);
+    const reasons = [];
+    if (daysLeft < 0) reasons.push('expired');
+    else if (daysLeft <= 3) reasons.push('expiringSoon');
+    if (it.qty < it.minQty) reasons.push('lowStock');
+    if (reasons.length === 0) continue;
+    tasks.push({ name: it.name, reasons, daysLeft, qty: it.qty, minQty: it.minQty, unit: it.unit });
+  }
+  return tasks.sort(compareUrgency);
+}
+
+// 두 task 중 어느 쪽이 더 급한지 정한다 (Array.prototype.sort 비교 함수).
+// a가 더 급하면 음수, b가 더 급하면 양수, 똑같으면 0을 반환.
+export function compareUrgency(a, b) {
+  // TODO(직접 구현): 급한 순서 규칙을 정해서 코드로 옮겨 보세요.
+  return 0;
+}
diff --git a/test/stock.test.mjs b/test/stock.test.mjs
index 71de632..3f2e2bf 100644
--- a/test/stock.test.mjs
+++ b/test/stock.test.mjs
@@ -1,5 +1,5 @@
 import assert from 'node:assert/strict';
-import { lowStock, orderList, daysBetween } from '../src/stock.mjs';
+import { lowStock, orderList, daysBetween, todayTasks } from '../src/stock.mjs';
 const items = [
   { name: '우유', qty: 3, unit: 'L', expires: '2026-10-20', minQty: 5 },
   { name: '원두', qty: 4, unit: 'kg', expires: '2027-01-10', minQty: 2 },
@@ -8,3 +8,49 @@ assert.deepEqual(lowStock(items), ['우유']);
 assert.deepEqual(orderList(items), [{ name: '우유', need: 7, unit: 'L' }]);
 assert.equal(daysBetween('2026-10-18', '2026-10-20'), 2);
 console.log('기본 테스트 통과');
+
+// --- todayTasks ---
+const today = '2026-10-18';
+const cafe = [
+  { name: '우유', qty: 3, unit: 'L', expires: '2026-10-20', minQty: 5 },        // 2일 남음 + 부족
+  { name: '생크림', qty: 2, unit: '팩', expires: '2026-10-18', minQty: 2 },     // 오늘까지
+  { name: '딸기', qty: 1, unit: 'kg', expires: '2026-10-17', minQty: 1 },       // 1일 지남
+  { name: '원두', qty: 4, unit: 'kg', expires: '2027-01-10', minQty: 2 },       // 해당 없음
+  { name: '바닐라 시럽', qty: 1, unit: '병', expires: '2026-10-21', minQty: 1 }, // 3일 남음 (경계)
+  { name: '치즈케이크', qty: 0, unit: '판', expires: '2026-10-19', minQty: 1 },  // 1일 남음 + 부족
+  { name: '종이컵', qty: 10, unit: '줄', expires: '2027-12-31', minQty: 20 },    // 부족만
+];
+const tasks = todayTasks(cafe, today);
+const byName = Object.fromEntries(tasks.map((t) => [t.name, t]));
+
+// 어떤 재료가 목록에 들어가는지
+assert.deepEqual(
+  tasks.map((t) => t.name).sort(),
+  ['딸기', '바닐라 시럽', '생크림', '우유', '종이컵', '치즈케이크'].sort(),
+);
+assert.equal(byName['원두'], undefined);
+
+// 이유 분류 (경계값: 오늘=0일, 3일 남음은 '곧 만료')
+assert.deepEqual(byName['딸기'].reasons, ['expired']);
+assert.deepEqual(byName['생크림'].reasons, ['expiringSoon']);
+assert.deepEqual(byName['바닐라 시럽'].reasons, ['expiringSoon']);
+assert.deepEqual(byName['종이컵'].reasons, ['lowStock']);
+
+// 한 재료가 두 이유에 걸려도 한 번만 나오고 이유는 둘 다
+assert.equal(tasks.filter((t) => t.name === '우유').length, 1);
+assert.deepEqual(byName['우유'].reasons, ['expiringSoon', 'lowStock']);
+assert.equal(byName['우유'].daysLeft, 2);
+
+// 빈 입력
+assert.deepEqual(todayTasks([], today), []);
+console.log('todayTasks 수집 테스트 통과');
+
+// 급한 순서 (compareUrgency를 구현해야 통과)
+const order = tasks.map((t) => t.name);
+const before = (x, y) => order.indexOf(x) < order.indexOf(y);
+assert.equal(order[0], '딸기', '유통기한 지난 것이 맨 앞');
+assert.ok(before('생크림', '바닐라 시럽'), '남은 날이 적을수록 먼저');
+assert.ok(before('치즈케이크', '바닐라 시럽'), '남은 날이 적을수록 먼저');
+// TODO(직접 추가): '곧 만료'와 '재고 부족' 사이 순서, 두 이유가 겹친 재료의 순서 등
+//   본인이 정한 규칙을 테스트로도 적어 보세요.
+console.log('todayTasks 순서 테스트 통과');