요청 2: 급한 순서를 정해야 하는 요청
요청문: src/stock.mjs에 todayTasks(items, today) 함수를 만들어 줘. 아침에 카페 사장님이 보는 '오늘 할 일' 목록이야. 유통기한이 지난 것, 3일 안에 끝나는 것, 수량이 minQty보다 적은 것을 한 목록으로 합쳐서 급한 것부터 보여 주고 싶어. today는 'YYYY-MM-DD' 문자열이야. test/stock.test.mjs에 테스트도 추가하고 돌려 봐.
diff --git a/src/stock.mjs b/src/stock.mjs
index f81b897..7fe1bc7 100644
--- a/src/stock.mjs
+++ b/src/stock.mjs
@@ -19,3 +19,40 @@ export function orderList(items) {
.filter((it) => it.qty < it.minQty)
.map((it) => ({ name: it.name, need: it.minQty * 2 - it.qty, unit: it.unit }));
}
+
+// 아침 '오늘 할 일' 목록. 한 품목에 사유가 여러 개면 한 줄로 합친다.
+// 급한 순서: 유통기한 지남(오래된 것부터) → 3일 이내 만료(임박한 것부터) → 재고 부족만(부족 비율 큰 것부터)
+// 수량이 0이면 버리거나 먼저 쓸 것이 없으므로 유통기한 사유는 붙이지 않는다.
+const SOON_DAYS = 3;
+
+export function todayTasks(items, today) {
+ const tasks = [];
+ for (const it of items) {
+ const daysLeft = daysBetween(today, it.expires);
+ const reasons = [];
+ const labels = [];
+ if (it.qty > 0 && daysLeft < 0) {
+ reasons.push('expired');
+ labels.push(`유통기한 ${-daysLeft}일 지남`);
+ } else if (it.qty > 0 && daysLeft <= SOON_DAYS) {
+ reasons.push('expiring');
+ labels.push(daysLeft === 0 ? '오늘까지' : `${daysLeft}일 남음`);
+ }
+ if (it.qty < it.minQty) {
+ reasons.push('low');
+ labels.push(`재고 부족 (${it.qty}/${it.minQty}${it.unit})`);
+ }
+ if (reasons.length) {
+ tasks.push({ name: it.name, reasons, label: labels.join(', '), daysLeft, qty: it.qty, minQty: it.minQty, unit: it.unit });
+ }
+ }
+ const rank = (t) => (t.reasons.includes('expired') ? 0 : t.reasons.includes('expiring') ? 1 : 2);
+ return tasks.sort(
+ (a, b) =>
+ rank(a) - rank(b) ||
+ (rank(a) < 2 ? a.daysLeft - b.daysLeft : 0) ||
+ b.reasons.length - a.reasons.length ||
+ a.qty / a.minQty - b.qty / b.minQty ||
+ a.name.localeCompare(b.name),
+ );
+}
diff --git a/test/stock.test.mjs b/test/stock.test.mjs
index 71de632..8ae38bc 100644
--- a/test/stock.test.mjs
+++ b/test/stock.test.mjs
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
-import { lowStock, orderList, daysBetween } from '../src/stock.mjs';
+import { readFileSync } from 'node:fs';
+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 +9,31 @@ 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: data.json 기준 2026-10-18 아침
+const data = JSON.parse(readFileSync(new URL('../data.json', import.meta.url), 'utf8'));
+const tasks = todayTasks(data, '2026-10-18');
+assert.deepEqual(
+ tasks.map((t) => [t.name, t.reasons, t.label]),
+ [
+ ['딸기', ['expired'], '유통기한 1일 지남'],
+ ['생크림', ['expiring'], '오늘까지'],
+ ['우유', ['expiring', 'low'], '2일 남음, 재고 부족 (3/5L)'],
+ ['바닐라 시럽', ['expiring'], '3일 남음'],
+ ['치즈케이크', ['low'], '재고 부족 (0/1판)'],
+ ],
+);
+
+// 경계: 4일 남으면 제외, 수량 0이면 유통기한 사유 없음, 재고 부족은 비율 큰 것부터
+const edge = todayTasks(
+ [
+ { name: 'A', qty: 5, unit: '개', expires: '2026-10-22', minQty: 1 },
+ { name: 'B', qty: 0, unit: '개', expires: '2026-10-10', minQty: 0 },
+ { name: 'C', qty: 3, unit: '개', expires: '2027-01-01', minQty: 4 },
+ { name: 'D', qty: 1, unit: '개', expires: '2027-01-01', minQty: 4 },
+ ],
+ '2026-10-18',
+);
+assert.deepEqual(edge.map((t) => t.name), ['D', 'C']);
+assert.deepEqual(todayTasks([], '2026-10-18'), []);
+console.log('todayTasks 테스트 통과');