feat: #280 찻잎 사용량 기록 추가#282
Conversation
- Note 엔티티에 teaLeafWeight DECIMAL(5,1) nullable 컬럼 추가 - TypeORM migration: AddTeaLeafWeightToNotes - CreateNoteDto에 @IsOptional @IsNumber teaLeafWeight 필드 추가 - Note 타입, useNoteForm, NewNote, NoteDetail 프론트엔드 구현 - Settings 페이지에 기본 찻잎 사용량 설정(localStorage) 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- teaLeafWeight DTO에 @Transform으로 null 보존 (enableImplicitConversion 대응) - migration 파일을 notes 테이블 변경만 포함하도록 최소화 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough차록(차 기록) 시스템에 찻잎 사용량(g) 기록 기능을 추가하는 변경사항입니다. 데이터베이스 스키마에 새로운 컬럼을 추가하고, 백엔드 API 계층과 프론트엔드 폼, 상세 페이지 및 설정 화면에 필드 및 UI를 통합하여 사용자가 찻잎 무게를 입력·조회하고 기본값을 설정할 수 있도록 구성했습니다. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/notes/dto/create-note.dto.ts`:
- Around line 104-105: The current `@Transform` uses parseFloat which accepts
strings like "7.5g"; update the transform on the CreateNote DTO property (the
`@Transform`(({ value }) => ... ) decorator) to only parse strings that fully
match a strict numeric pattern (e.g. use a regex like /^-?\d+(\.\d+)?$/) and
otherwise return the original value (or null/undefined) so the subsequent
`@IsNumber` validation can reject invalid input; in short, replace
parseFloat(value) with a guarded parse that first tests the entire string
against the strict numeric regex before calling parseFloat.
In `@src/hooks/useNoteForm.ts`:
- Around line 336-337: The payload currently uses "teaLeafWeight: teaLeafWeight
?? undefined", which drops the field from the serialized body when null and
prevents clearing the value on PATCH; update the payload construction in
useNoteForm (the spots where teaLeafWeight is set, including the occurrences at
the two locations flagged) to always include the teaLeafWeight property and send
explicit null when the user cleared the field (e.g., set the property to null if
the local value is null, otherwise the numeric value) so UpdateNoteDto will
receive a null and clear the stored value instead of leaving it unchanged.
- Around line 44-48: In useNoteForm, prevent storing NaN/Infinity by validating
parseFloat results for teaLeafWeight before calling setTeaLeafWeight (and when
initializing from localStorage) and when mapping API responses (the same
parseFloat usage around teaLeafWeight/state mapping); ensure the parsed value is
finite (Number.isFinite), within the allowed range (>= 0.1 && <= 999.9), and
only then set state, otherwise fall back to null (or a safe default) and log or
ignore invalid values.
In `@src/pages/NewNote.tsx`:
- Around line 181-184: The onChange handler for tea leaf weight uses parseFloat
and can set NaN into state; update the handler (the function that calls
setTeaLeafWeight) to parse the input safely (e.g., trim the value, skip empty
string) and then check the parsed number with Number.isFinite or isNaN — if the
parse yields NaN, call setTeaLeafWeight(null), otherwise set the parsed numeric
value; keep the existing empty-string branch intact so intermediate input never
stores NaN in teaLeafWeight.
In `@src/pages/Settings.tsx`:
- Around line 58-61: The state initializer for defaultTeaLeafWeight uses
parseFloat on localStorage value but doesn't guard against NaN; update the
initializer and any places that parse and save the value (including
setDefaultTeaLeafWeightState usage and the code around the other parseFloat at
lines mentioned) to check isNaN(parsed) and treat NaN as null (and avoid writing
"NaN" to localStorage), i.e., parse the stored string with parseFloat, if
Number.isNaN(result) then return null and when persisting only save valid
numeric values (or remove the key) so input-control and autofill logic won't
receive NaN.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0fe4402c-b7bb-4fa0-aacd-bf79d5434b9c
📒 Files selected for processing (10)
backend/migrations/1773820392689-AddTeaLeafWeightToNotes.tsbackend/src/notes/dto/create-note.dto.tsbackend/src/notes/entities/note.entity.tsbackend/test/suites/notes-crud.e2e-spec.tssrc/hooks/useNoteForm.tssrc/lib/api/notes.api.tssrc/pages/NewNote.tsxsrc/pages/NoteDetail.tsxsrc/pages/Settings.tsxsrc/types/index.ts
| @Transform(({ value }) => value === null || value === undefined ? value : (typeof value === 'string' ? parseFloat(value) : value)) | ||
| @IsNumber({ maxDecimalPlaces: 1 }) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In JavaScript, what do parseFloat("7.5g"), parseFloat("1abc"), and Number("7.5g") return, and why?
💡 Result:
parseFloat("7.5g") returns 7.5, parseFloat("1abc") returns 1, and Number("7.5g") returns NaN. parseFloat parses from the start of the string until it encounters an invalid character (not +, -, 0-9, ., e/E), returning the numeric value up to that point. For "7.5g", it parses "7.5" (valid float) and stops at 'g'. For "1abc", it parses "1" and stops at 'a'. Number attempts to convert the entire string to a valid number literal; any invalid characters anywhere result in NaN. "7.5g" and "1abc" are invalid full literals.
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat
- 2: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
- 3: https://mdn2.netlify.app/en-us/docs/web/javascript/reference/global_objects/parsefloat/
- 4: https://stackoverflow.com/questions/46621185/what-is-the-critical-difference-between-number-parseint-number-parsefloat
- 5: https://stackoverflow.com/questions/11988547/what-is-the-difference-between-number-and-parsefloat
- 6: https://medium.com/@maxheadway/the-differences-between-number-and-parsefloat-in-javascript-8ee74b961ed4
🏁 Script executed:
#!/bin/bash
# Check if the file exists and read the relevant section
if [ -f "backend/src/notes/dto/create-note.dto.ts" ]; then
echo "=== File found, reading lines 100-115 ==="
sed -n '100,115p' "backend/src/notes/dto/create-note.dto.ts" | cat -n
else
echo "File not found at expected path"
# Try to locate it
find . -name "create-note.dto.ts" -type f
fiRepository: FLYLIKEB/ChaLog
Length of output: 572
parseFloat 변환이 너무 관대해서 잘못된 입력이 통과할 수 있습니다.
Line 105의 parseFloat는 "7.5g" 같은 값을 7.5로 변환해 Line 106의 @IsNumber 검증을 통과시킬 수 있습니다. 숫자 형식을 엄격하게 검증하도록 변환 로직을 수정해주세요.
🔧 제안 수정
`@IsOptional`()
- `@Transform`(({ value }) => value === null || value === undefined ? value : (typeof value === 'string' ? parseFloat(value) : value))
+ `@Transform`(({ value }) => {
+ if (value === null || value === undefined) return value;
+ if (typeof value !== 'string') return value;
+ const trimmed = value.trim();
+ // 숫자 + 소수점 이하 1자리까지만 허용 (예: 5, 5.5, 7.5)
+ if (!/^\d+(\.\d)?$/.test(trimmed)) return value; // IsNumber가 실패하도록 원문 유지
+ return Number(trimmed);
+ })
`@IsNumber`({ maxDecimalPlaces: 1 })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Transform(({ value }) => value === null || value === undefined ? value : (typeof value === 'string' ? parseFloat(value) : value)) | |
| @IsNumber({ maxDecimalPlaces: 1 }) | |
| `@IsOptional`() | |
| `@Transform`(({ value }) => { | |
| if (value === null || value === undefined) return value; | |
| if (typeof value !== 'string') return value; | |
| const trimmed = value.trim(); | |
| // 숫자 + 소수점 이하 1자리까지만 허용 (예: 5, 5.5, 7.5) | |
| if (!/^\d+(\.\d)?$/.test(trimmed)) return value; // IsNumber가 실패하도록 원문 유지 | |
| return Number(trimmed); | |
| }) | |
| `@IsNumber`({ maxDecimalPlaces: 1 }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/notes/dto/create-note.dto.ts` around lines 104 - 105, The current
`@Transform` uses parseFloat which accepts strings like "7.5g"; update the
transform on the CreateNote DTO property (the `@Transform`(({ value }) => ... )
decorator) to only parse strings that fully match a strict numeric pattern (e.g.
use a regex like /^-?\d+(\.\d+)?$/) and otherwise return the original value (or
null/undefined) so the subsequent `@IsNumber` validation can reject invalid input;
in short, replace parseFloat(value) with a guarded parse that first tests the
entire string against the strict numeric regex before calling parseFloat.
| const [teaLeafWeight, setTeaLeafWeight] = useState<number | null>(() => { | ||
| if (mode === 'edit') return null; | ||
| const stored = localStorage.getItem('defaultTeaLeafWeight'); | ||
| return stored ? parseFloat(stored) : null; | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/hooks/useNoteForm.ts | head -200Repository: FLYLIKEB/ChaLog
Length of output: 8457
🏁 Script executed:
grep -n "parseFloat" src/hooks/useNoteForm.tsRepository: FLYLIKEB/ChaLog
Length of output: 169
🏁 Script executed:
fd -t f "types\.(ts|tsx)$" src/ --exec cat -n {}Repository: FLYLIKEB/ChaLog
Length of output: 41
🏁 Script executed:
rg "interface Note|type Note" src/types --type ts -A 20Repository: FLYLIKEB/ChaLog
Length of output: 966
🏁 Script executed:
rg "teaLeafWeight" src/types --type ts -B 2 -A 2Repository: FLYLIKEB/ChaLog
Length of output: 281
🏁 Script executed:
rg "teaLeafWeight|WEIGHT|weight" src/ --type ts -B 2 -A 2 | grep -v "node_modules"Repository: FLYLIKEB/ChaLog
Length of output: 22302
🏁 Script executed:
rg "teaLeafWeight" src/pages/NewNote.tsx -B 5 -A 5Repository: FLYLIKEB/ChaLog
Length of output: 691
🏁 Script executed:
rg "teaLeafWeight" src/lib/api --type ts -B 3 -A 3Repository: FLYLIKEB/ChaLog
Length of output: 375
🏁 Script executed:
rg "0\.1|999\.9|teaLeafWeight|TEA.*WEIGHT" src/ --type ts -B 2 -A 2Repository: FLYLIKEB/ChaLog
Length of output: 8817
parseFloat 기반 초기화/매핑은 NaN 및 느슨한 파싱을 유입시킬 수 있습니다.
Line 47, Line 179에서 parseFloat 결과를 검증 없이 상태로 넣고 있어, 비정상 문자열(예: "abc", "Infinity")이 들어오면 NaN이나 Infinity가 저장될 수 있습니다. HTML 입력 제약(min="0.1", max="999.9", step="0.1")은 UI 레벨에서만 작동하며, localStorage와 API 응답 경로에서는 보호되지 않습니다. 상태 설정 전 유효 범위와 유한수 검증이 필요합니다.
🔧 제안 수정
+ const parseTeaLeafWeight = (raw: unknown): number | null => {
+ if (raw == null || raw === '') return null;
+ const n = typeof raw === 'string' ? Number(raw.trim()) : Number(raw);
+ if (!Number.isFinite(n)) return null;
+ if (n < 0.1 || n > 999.9) return null;
+ if (!Number.isInteger(n * 10)) return null; // 소수 첫째 자리까지만 허용
+ return n;
+ };
+
const [teaLeafWeight, setTeaLeafWeight] = useState<number | null>(() => {
if (mode === 'edit') return null;
const stored = localStorage.getItem('defaultTeaLeafWeight');
- return stored ? parseFloat(stored) : null;
+ return parseTeaLeafWeight(stored);
});
...
if (normalizedNote.teaLeafWeight != null) {
- setTeaLeafWeight(
- typeof normalizedNote.teaLeafWeight === 'string'
- ? parseFloat(normalizedNote.teaLeafWeight)
- : normalizedNote.teaLeafWeight,
- );
+ setTeaLeafWeight(parseTeaLeafWeight(normalizedNote.teaLeafWeight));
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useNoteForm.ts` around lines 44 - 48, In useNoteForm, prevent
storing NaN/Infinity by validating parseFloat results for teaLeafWeight before
calling setTeaLeafWeight (and when initializing from localStorage) and when
mapping API responses (the same parseFloat usage around teaLeafWeight/state
mapping); ensure the parsed value is finite (Number.isFinite), within the
allowed range (>= 0.1 && <= 999.9), and only then set state, otherwise fall back
to null (or a safe default) and log or ignore invalid values.
| teaLeafWeight: teaLeafWeight ?? undefined, | ||
| isPublic, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
# 1) Update DTO가 PartialType인지 확인 (누락 필드 미갱신 근거)
sed -n '1,80p' backend/src/notes/dto/update-note.dto.ts
# 2) useNoteForm 저장 payload에서 null -> undefined 변환 사용 위치 확인
rg -n -C2 'teaLeafWeight:\s*teaLeafWeight\s*\?\?\s*undefined' src/hooks/useNoteForm.tsRepository: FLYLIKEB/ChaLog
Length of output: 635
🏁 Script executed:
# 1. teaLeafWeight 상태 초기화 및 타입 확인
rg -n "teaLeafWeight" src/hooks/useNoteForm.ts | head -20
# 2. 페이로드가 어떻게 구성되는지 확인 (전체 save 함수 구조)
sed -n '320,380p' src/hooks/useNoteForm.ts
# 3. 페이로드가 API로 전송될 때 undefined 처리 방식 확인
sed -n '380,420p' src/hooks/useNoteForm.tsRepository: FLYLIKEB/ChaLog
Length of output: 3476
🏁 Script executed:
# 1. notesApi 구현 위치 찾기
find src -name "*api*" -o -name "*notes*" | grep -E "\.(ts|tsx)$"
# 2. notesApi.create/update 구현 확인
rg -n "notesApi\s*=" src/hooks/useNoteForm.ts
# 3. API 클라이언트 구현
rg -n "export.*notesApi" src -A 5Repository: FLYLIKEB/ChaLog
Length of output: 1372
🏁 Script executed:
# notes.api.ts에서 create/update 메서드 확인
cat src/lib/api/notes.api.ts | head -100Repository: FLYLIKEB/ChaLog
Length of output: 3785
🏁 Script executed:
# apiClient 구현 확인
cat src/lib/api/client.ts | head -150Repository: FLYLIKEB/ChaLog
Length of output: 4610
🏁 Script executed:
# apiClient의 post/patch 메서드 구현 확인
tail -n +150 src/lib/api/client.ts | head -200Repository: FLYLIKEB/ChaLog
Length of output: 5511
🏁 Script executed:
# POST/PATCH 메서드 구현 찾기
rg -n "^\s*post:|^\s*patch:" src/lib/api/client.ts -A 5Repository: FLYLIKEB/ChaLog
Length of output: 41
🏁 Script executed:
# apiClient 객체 정의 찾기
rg -n "export.*apiClient" src/lib/api/client.ts -A 30Repository: FLYLIKEB/ChaLog
Length of output: 117
🏁 Script executed:
# ApiClient 클래스 정의 찾기
rg -n "class ApiClient" src/lib/api/client.ts -A 50Repository: FLYLIKEB/ChaLog
Length of output: 2154
🏁 Script executed:
# ApiClient 클래스 내 post/patch 메서드 찾기
rg -n "^\s*(post|patch)\s*<" src/lib/api/client.ts -A 8Repository: FLYLIKEB/ChaLog
Length of output: 41
🏁 Script executed:
# post와 patch 메서드 찾기 (더 간단한 패턴)
sed -n '384,900p' src/lib/api/client.ts | grep -n -A 10 "post\|patch"Repository: FLYLIKEB/ChaLog
Length of output: 1027
수정(PATCH) 시 teaLeafWeight 필드를 삭제할 수 없습니다.
Line 336과 362에서 teaLeafWeight ?? undefined를 사용하면, teaLeafWeight가 null일 때 undefined로 변환됩니다. JavaScript의 JSON.stringify()는 undefined 속성을 직렬화에서 제외하므로 해당 필드가 요청 바디에 포함되지 않습니다. NestJS의 PartialType을 사용하는 UpdateNoteDto에서는 요청에 없는 필드를 갱신하지 않으므로 기존 값이 유지됩니다. 따라서 사용자가 한 번 설정한 값을 이후 수정 시 지울 수 없습니다.
제안 수정
- teaLeafWeight: teaLeafWeight ?? undefined,
+ teaLeafWeight,(Line 362도 동일하게 수정 필요)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| teaLeafWeight: teaLeafWeight ?? undefined, | |
| isPublic, | |
| teaLeafWeight, | |
| isPublic, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useNoteForm.ts` around lines 336 - 337, The payload currently uses
"teaLeafWeight: teaLeafWeight ?? undefined", which drops the field from the
serialized body when null and prevents clearing the value on PATCH; update the
payload construction in useNoteForm (the spots where teaLeafWeight is set,
including the occurrences at the two locations flagged) to always include the
teaLeafWeight property and send explicit null when the user cleared the field
(e.g., set the property to null if the local value is null, otherwise the
numeric value) so UpdateNoteDto will receive a null and clear the stored value
instead of leaving it unchanged.
| onChange={(e) => { | ||
| const val = e.target.value; | ||
| setTeaLeafWeight(val === '' ? null : parseFloat(val)); | ||
| }} |
There was a problem hiding this comment.
찻잎 사용량 입력에서 NaN이 상태로 들어갈 수 있습니다.
Line 183의 parseFloat는 입력 중간 상태에서 NaN을 만들 수 있어, 저장 시 잘못된 payload가 전송될 수 있습니다.
🔧 제안 수정안
- onChange={(e) => {
- const val = e.target.value;
- setTeaLeafWeight(val === '' ? null : parseFloat(val));
- }}
+ onChange={(e) => {
+ const raw = e.currentTarget.value;
+ const next = e.currentTarget.valueAsNumber;
+ if (raw === '' || Number.isNaN(next)) {
+ setTeaLeafWeight(null);
+ return;
+ }
+ setTeaLeafWeight(Math.round(next * 10) / 10);
+ }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onChange={(e) => { | |
| const val = e.target.value; | |
| setTeaLeafWeight(val === '' ? null : parseFloat(val)); | |
| }} | |
| onChange={(e) => { | |
| const raw = e.currentTarget.value; | |
| const next = e.currentTarget.valueAsNumber; | |
| if (raw === '' || Number.isNaN(next)) { | |
| setTeaLeafWeight(null); | |
| return; | |
| } | |
| setTeaLeafWeight(Math.round(next * 10) / 10); | |
| }} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/NewNote.tsx` around lines 181 - 184, The onChange handler for tea
leaf weight uses parseFloat and can set NaN into state; update the handler (the
function that calls setTeaLeafWeight) to parse the input safely (e.g., trim the
value, skip empty string) and then check the parsed number with Number.isFinite
or isNaN — if the parse yields NaN, call setTeaLeafWeight(null), otherwise set
the parsed numeric value; keep the existing empty-string branch intact so
intermediate input never stores NaN in teaLeafWeight.
| const [defaultTeaLeafWeight, setDefaultTeaLeafWeightState] = useState<number | null>(() => { | ||
| const stored = localStorage.getItem('defaultTeaLeafWeight'); | ||
| return stored ? parseFloat(stored) : null; | ||
| }); |
There was a problem hiding this comment.
NaN 값이 기본값 상태와 localStorage에 저장될 수 있습니다.
Line 60/Line 596의 parseFloat 결과가 NaN일 때도 그대로 저장되어, 입력값 제어와 기본값 자동 채움이 깨질 수 있습니다.
🔧 제안 수정안
- const [defaultTeaLeafWeight, setDefaultTeaLeafWeightState] = useState<number | null>(() => {
- const stored = localStorage.getItem('defaultTeaLeafWeight');
- return stored ? parseFloat(stored) : null;
- });
+ const parseTeaLeafWeight = (raw: string | null): number | null => {
+ if (raw == null || raw.trim() === '') return null;
+ const n = Number(raw);
+ if (!Number.isFinite(n)) return null;
+ if (n < 0.1 || n > 999.9) return null;
+ return Math.round(n * 10) / 10;
+ };
+
+ const [defaultTeaLeafWeight, setDefaultTeaLeafWeightState] = useState<number | null>(() =>
+ parseTeaLeafWeight(localStorage.getItem('defaultTeaLeafWeight')),
+ );
@@
- onChange={(e) => {
- const val = e.target.value;
- const parsed = val === '' ? null : parseFloat(val);
- setDefaultTeaLeafWeightState(parsed);
- if (parsed === null) {
+ onChange={(e) => {
+ const raw = e.currentTarget.value;
+ const parsed = raw === '' ? null : parseTeaLeafWeight(raw);
+ setDefaultTeaLeafWeightState(parsed);
+ if (parsed == null) {
localStorage.removeItem('defaultTeaLeafWeight');
} else {
localStorage.setItem('defaultTeaLeafWeight', String(parsed));
}
}}Also applies to: 594-602
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Settings.tsx` around lines 58 - 61, The state initializer for
defaultTeaLeafWeight uses parseFloat on localStorage value but doesn't guard
against NaN; update the initializer and any places that parse and save the value
(including setDefaultTeaLeafWeightState usage and the code around the other
parseFloat at lines mentioned) to check isNaN(parsed) and treat NaN as null (and
avoid writing "NaN" to localStorage), i.e., parse the stored string with
parseFloat, if Number.isNaN(result) then return null and when persisting only
save valid numeric values (or remove the key) so input-control and autofill
logic won't receive NaN.
Summary
Test plan
npm run build✅cd backend && npm run build✅🤖 Generated with Claude Code
Summary by CodeRabbit
릴리스 노트
새로운 기능
테스트