feat(data): chosenAnswer + solve route + profile tabs — 7I.2

This commit is contained in:
reloop
2026-04-12 13:35:39 +09:00
parent e76e14eb1d
commit 4c61b87052
9 changed files with 1071 additions and 925 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `study_logs` ADD COLUMN `chosenAnswer` INTEGER NULL;

View File

@@ -123,6 +123,7 @@ model StudyLog {
difficulty Float
baseCorrectRate Float?
result StudyResult
chosenAnswer Int?
memo String? @db.Text
studiedAt DateTime @default(now())
timeSpent Int?

View File

@@ -27,7 +27,10 @@ import { StudyResult } from '@prisma/client';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { AuthUser } from '../auth/jwt.strategy';
import { StudyLogsService } from './study-logs.service';
import {
StudyLogsService,
type UpdateStudyLogInput,
} from './study-logs.service';
class CreateStudyLogDto {
@IsInt()
@@ -58,6 +61,13 @@ class CreateStudyLogDto {
@IsEnum(StudyResult)
result: StudyResult;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(5)
chosenAnswer?: number;
@IsOptional()
@IsString()
memo?: string;
@@ -129,7 +139,18 @@ class CreateFromProblemSetDto {
answers: ProblemSetAnswerDto[];
}
class UpdateStudyLogDto {
class UpdateStudyLogDto implements UpdateStudyLogInput {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(5)
chosenAnswer?: number;
@IsOptional()
@IsEnum(StudyResult)
result?: StudyResult;
@IsOptional()
@IsString()
memo?: string;

View File

@@ -16,10 +16,17 @@ export interface CreateStudyLogInput {
difficulty: number;
baseCorrectRate?: number | null;
result: StudyResult;
chosenAnswer?: number | null;
memo?: string;
timeSpent?: number;
}
export interface UpdateStudyLogInput {
chosenAnswer?: number;
result?: StudyResult;
memo?: string;
}
export interface CreateFromProblemSetInput {
problemSetId: number;
totalTimeSpent: number;
@@ -167,6 +174,7 @@ export class StudyLogsService {
difficulty: problem.difficulty,
baseCorrectRate: problem.baseCorrectRate,
result: derivedResult,
chosenAnswer,
memo:
problem.answerNumber === null
? '정답 미등록 (자동 채점 불가)'
@@ -269,6 +277,7 @@ export class StudyLogsService {
id: log.id,
studiedAt: log.studiedAt,
result: log.result,
chosenAnswer: log.chosenAnswer,
memo: log.memo,
timeSpent: log.timeSpent,
difficulty: log.difficulty,
@@ -284,6 +293,7 @@ export class StudyLogsService {
choices: log.problem.choices,
answerNumber: log.problem.answerNumber,
imageUrl: log.problem.imageUrl,
pageImageUrl: log.problem.pageImageUrl,
problemSet: log.problem.problemSet,
}
: null,
@@ -299,7 +309,7 @@ export class StudyLogsService {
};
}
async update(userId: number, id: number, data: { memo?: string }) {
async update(userId: number, id: number, data: UpdateStudyLogInput) {
const existing = await this.prisma.studyLog.findFirst({
where: { id, userId },
select: { id: true },
@@ -309,10 +319,16 @@ export class StudyLogsService {
return this.prisma.studyLog.update({
where: { id },
data: {
memo: data.memo?.trim() ? data.memo : null,
...(data.memo !== undefined && {
memo: data.memo.trim() ? data.memo : null,
}),
...(data.result !== undefined && { result: data.result }),
...(data.chosenAnswer !== undefined && { chosenAnswer: data.chosenAnswer }),
},
select: {
id: true,
chosenAnswer: true,
result: true,
memo: true,
},
});
@@ -365,6 +381,7 @@ export class StudyLogsService {
difficulty: clamp01(input.difficulty),
baseCorrectRate: input.baseCorrectRate ?? null,
result: input.result,
chosenAnswer: input.chosenAnswer ?? null,
memo: input.memo ?? null,
timeSpent: input.timeSpent ?? null,
},

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,481 @@
'use client';
import React, { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import styled from 'styled-components';
import AppShell from '@/components/layout/AppShell';
import { Icon } from '@/components/ui/Icon';
import { Button, Card } from '@/components/ui/primitives';
import {
getStudyLog,
resolveUploadUrl,
updateStudyLog,
type StudyLogDetail,
type StudyResult,
} from '@/lib/api';
import { theme } from '@/styles/theme';
export default function SolveStudyLogPage() {
return (
<AppShell>
<SolveStudyLogBody />
</AppShell>
);
}
function SolveStudyLogBody() {
const params = useParams<{ studyLogId: string }>();
const studyLogId = Number(params.studyLogId);
const [detail, setDetail] = useState<StudyLogDetail | null>(null);
const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null);
const [submitted, setSubmitted] = useState(false);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function load() {
if (!Number.isFinite(studyLogId)) {
setError('잘못된 학습 기록 주소입니다.');
setLoading(false);
return;
}
setLoading(true);
setError(null);
try {
const response = await getStudyLog(studyLogId);
if (cancelled) return;
setDetail(response);
setSelectedAnswer(response.chosenAnswer);
} catch {
if (cancelled) return;
setError('문제를 불러오지 못했어요.');
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
void load();
return () => {
cancelled = true;
};
}, [studyLogId]);
const problem = detail?.problem ?? null;
const choices = useMemo(() => normalizeChoices(problem?.choices), [problem?.choices]);
const problemImageUrl = resolveUploadUrl(problem?.imageUrl ?? problem?.pageImageUrl);
const problemBodyText = normalizeOptionalText(problem?.bodyText);
const result = useMemo(() => {
if (!problem || selectedAnswer === null) return null;
return computeResult(selectedAnswer, problem.answerNumber);
}, [problem, selectedAnswer]);
const submitAnswer = async () => {
if (!detail || !problem || selectedAnswer === null || saving) return;
setSaving(true);
setError(null);
try {
const nextResult = computeResult(selectedAnswer, problem.answerNumber);
const response = await updateStudyLog(detail.id, {
chosenAnswer: selectedAnswer,
result: nextResult,
});
setDetail((prev) =>
prev
? {
...prev,
chosenAnswer: response.chosenAnswer,
result: response.result,
memo: response.memo,
}
: prev,
);
setSubmitted(true);
} catch {
setError('답안을 저장하지 못했어요.');
} finally {
setSaving(false);
}
};
if (loading) {
return <StateCard> ...</StateCard>;
}
if (error || !detail || !problem) {
return (
<StateCard>
<Icon name="info" size={18} />
{error ?? '학습 기록에 연결된 문제가 없습니다.'}
</StateCard>
);
}
return (
<Page>
<HeaderCard>
<Eyebrow>
{problem.problemSet.year} {problem.problemSet.examType} · {problem.problemSet.subjectName}
</Eyebrow>
<Title>{problem.number} </Title>
<MetaLine>
{problem.problemSet.title}
{detail.tag ? ` · ${detail.tag.name}` : ''}
</MetaLine>
</HeaderCard>
<Layout>
<ProblemCard>
{problemImageUrl ? (
<ProblemImage
src={problemImageUrl}
alt={`${problem.problemSet.subjectName} ${problem.number}`}
/>
) : null}
{problemBodyText ? (
<ProblemBody $muted={Boolean(problemImageUrl)}>{problemBodyText}</ProblemBody>
) : !problemImageUrl ? (
<ProblemBody> .</ProblemBody>
) : null}
</ProblemCard>
<AnswerCard>
<AnswerHeader>
<div>
<SectionTitle> </SectionTitle>
<SectionMeta>
{choices.length > 0
? '선택 후 제출하면 학습 기록의 chosenAnswer/result가 갱신됩니다.'
: '선택지가 없는 문제라 답안 선택 없이 이미지만 다시 볼 수 있습니다.'}
</SectionMeta>
</div>
<Link href={`/study-logs/${detail.id}`}>
<BackLink> </BackLink>
</Link>
</AnswerHeader>
{choices.length > 0 ? (
<ChoiceList>
{choices.map((choice) => {
const selected = selectedAnswer === choice.number;
return (
<ChoiceButton
key={choice.number}
type="button"
$selected={selected}
disabled={submitted}
onClick={() => setSelectedAnswer(choice.number)}
>
<ChoiceNumber $selected={selected}>{choice.number}</ChoiceNumber>
<ChoiceText>{choice.text}</ChoiceText>
</ChoiceButton>
);
})}
</ChoiceList>
) : null}
{submitted && result ? (
<ResultOverlay $result={result}>
<ResultTitle>
<Icon
name={result === 'correct' ? 'check-circle' : result === 'incorrect' ? 'x' : 'triangle'}
size={18}
weight="fill"
/>
{resultLabel(result)}
</ResultTitle>
<ResultBody>
{problem.answerNumber === null
? '정답 번호가 없어 partial로 저장했습니다.'
: `정답은 ${problem.answerNumber}번입니다.`}
</ResultBody>
<Link href={`/study-logs/${detail.id}`}>
<Button as="span"> </Button>
</Link>
</ResultOverlay>
) : (
<SubmitRow>
<SubmitSummary>
{selectedAnswer === null
? '답안을 선택해 주세요.'
: `${selectedAnswer}번 선택`}
</SubmitSummary>
<Button
type="button"
onClick={() => void submitAnswer()}
disabled={selectedAnswer === null || saving || choices.length === 0}
>
{saving ? '제출 중...' : '답안 제출'}
</Button>
</SubmitRow>
)}
</AnswerCard>
</Layout>
</Page>
);
}
function normalizeChoices(
choices: Record<string, string> | null | undefined,
): Array<{ number: number; text: string }> {
if (!choices) return [];
return Object.entries(choices)
.map(([key, text]) => ({ number: Number(key), text }))
.filter((choice) => Number.isFinite(choice.number))
.sort((a, b) => a.number - b.number);
}
function normalizeOptionalText(value: string | null | undefined): string | null {
if (!value) return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function computeResult(
chosenAnswer: number,
answerNumber: number | null,
): StudyResult {
if (answerNumber === null) return 'partial';
return chosenAnswer === answerNumber ? 'correct' : 'incorrect';
}
function resultLabel(result: StudyResult): string {
if (result === 'correct') return '정답입니다';
if (result === 'incorrect') return '오답입니다';
return '정답 정보가 없습니다';
}
const Page = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
`;
const HeaderCard = styled(Card)`
border-radius: 24px;
border: 1px solid ${theme.color.borderSoftAlpha};
background:
radial-gradient(circle at top right, rgba(99, 102, 241, 0.18), transparent 30%),
rgba(21, 21, 28, 0.88);
`;
const Eyebrow = styled.div`
color: #a5b4fc;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
`;
const Title = styled.h1`
margin: 8px 0 0;
color: ${theme.color.textBright};
font-family: ${theme.font.display};
font-size: 30px;
line-height: 1.12;
`;
const MetaLine = styled.p`
margin: 10px 0 0;
color: ${theme.color.textSub};
font-size: 14px;
`;
const Layout = styled.div`
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.95fr);
gap: 20px;
@media (max-width: ${theme.breakpoint.desktop}) {
grid-template-columns: 1fr;
}
`;
const ProblemCard = styled(Card)`
border-radius: 24px;
border: 1px solid ${theme.color.borderSoftAlpha};
background: rgba(21, 21, 28, 0.88);
`;
const AnswerCard = styled(ProblemCard)`
position: relative;
overflow: hidden;
`;
const AnswerHeader = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
@media (max-width: ${theme.breakpoint.mobile}) {
flex-direction: column;
}
`;
const SectionTitle = styled.h2`
margin: 0;
color: ${theme.color.textBright};
font-size: 18px;
`;
const SectionMeta = styled.p`
margin: 8px 0 0;
color: ${theme.color.textSub};
font-size: 13px;
line-height: 1.6;
`;
const BackLink = styled.span`
color: #c7d2fe;
font-size: 13px;
font-weight: 600;
`;
const ProblemImage = styled.img`
display: block;
width: 100%;
border-radius: 18px;
border: 1px solid ${theme.color.borderSoftAlpha};
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.24);
background: rgba(15, 23, 42, 0.72);
`;
const ProblemBody = styled.div<{ $muted?: boolean }>`
margin-top: 16px;
white-space: pre-wrap;
color: ${({ $muted }) => ($muted ? theme.color.textSub : theme.color.textMain)};
line-height: 1.8;
font-size: 15px;
`;
const ChoiceList = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
`;
const ChoiceButton = styled.button<{ $selected: boolean }>`
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid
${({ $selected }) =>
$selected ? 'rgba(99, 102, 241, 0.48)' : theme.color.borderSoftAlpha};
background: ${({ $selected }) =>
$selected ? 'rgba(79, 70, 229, 0.14)' : 'rgba(255, 255, 255, 0.03)'};
color: ${theme.color.textMain};
cursor: pointer;
transition:
transform 0.18s ease,
border-color 0.18s ease,
background 0.18s ease;
&:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(99, 102, 241, 0.42);
}
&:disabled {
cursor: default;
opacity: 0.82;
}
`;
const ChoiceNumber = styled.span<{ $selected: boolean }>`
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 999px;
background: ${({ $selected }) =>
$selected ? 'rgba(79, 70, 229, 0.45)' : theme.color.surface2};
color: ${theme.color.textBright};
font-weight: 700;
box-shadow: ${({ $selected }) =>
$selected ? '0 0 0 3px rgba(99, 102, 241, 0.22)' : 'none'};
`;
const ChoiceText = styled.span`
flex: 1;
text-align: left;
line-height: 1.6;
`;
const SubmitRow = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 20px;
@media (max-width: ${theme.breakpoint.mobile}) {
flex-direction: column;
align-items: stretch;
}
`;
const SubmitSummary = styled.div`
color: ${theme.color.textSub};
font-size: 13px;
`;
const ResultOverlay = styled.div<{ $result: StudyResult }>`
display: flex;
flex-direction: column;
gap: 12px;
padding: 18px;
border-radius: 20px;
margin-top: 20px;
border: 1px solid ${({ $result }) => {
if ($result === 'correct') return 'rgba(16, 185, 129, 0.42)';
if ($result === 'incorrect') return 'rgba(244, 63, 94, 0.4)';
return 'rgba(250, 204, 21, 0.4)';
}};
background: ${({ $result }) => {
if ($result === 'correct') return 'rgba(16, 185, 129, 0.12)';
if ($result === 'incorrect') return 'rgba(244, 63, 94, 0.1)';
return 'rgba(250, 204, 21, 0.1)';
}};
`;
const ResultTitle = styled.div`
display: inline-flex;
align-items: center;
gap: 8px;
color: ${theme.color.textBright};
font-size: 18px;
font-weight: 700;
`;
const ResultBody = styled.p`
margin: 0;
color: ${theme.color.textSub};
line-height: 1.6;
`;
const StateCard = styled(Card)`
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 180px;
justify-content: center;
border-radius: 24px;
color: ${theme.color.textSub};
`;

View File

@@ -8,8 +8,9 @@ import AppShell from '@/components/layout/AppShell';
import { Icon } from '@/components/ui/Icon';
import { Button, Card, Textarea } from '@/components/ui/primitives';
import {
api,
getStudyLog,
resolveUploadUrl,
updateStudyLog,
type ReviewStatus,
type StudyLogDetail,
type StudyResult,
@@ -47,10 +48,10 @@ function StudyLogDetailBody() {
setError(null);
try {
const response = await api.get<StudyLogDetail>(`/study-logs/${studyLogId}`);
const response = await getStudyLog(studyLogId);
if (cancelled) return;
setDetail(response.data);
setMemo(response.data.memo ?? '');
setDetail(response);
setMemo(response.memo ?? '');
} catch {
if (cancelled) return;
setError('학습 기록 상세를 불러오지 못했어요.');
@@ -76,12 +77,18 @@ function StudyLogDetailBody() {
if (!detail) return;
setSaving(true);
try {
const response = await api.patch<{ id: number; memo: string | null }>(
`/study-logs/${detail.id}`,
{ memo },
const response = await updateStudyLog(detail.id, { memo });
setDetail((prev) =>
prev
? {
...prev,
memo: response.memo,
result: response.result,
chosenAnswer: response.chosenAnswer,
}
: prev,
);
setDetail((prev) => (prev ? { ...prev, memo: response.data.memo } : prev));
setMemo(response.data.memo ?? '');
setMemo(response.memo ?? '');
} catch {
setError('메모 저장에 실패했어요.');
} finally {
@@ -125,7 +132,7 @@ function StudyLogDetailBody() {
</MetaLine>
</div>
{problem && problemSet && (
<Link href={`/study/exam/${problemSet.id}?problem=${problem.number}`}>
<Link href={`/solve/${detail.id}`}>
<ReplayButton as="span" $variant="secondary">
<Icon name="arrow-right" size={16} />
@@ -154,10 +161,29 @@ function StudyLogDetailBody() {
<ChoiceList>
{problemChoices.map((choice) => {
const isCorrect = choice.number === problem?.answerNumber;
const isChosen = choice.number === detail.chosenAnswer;
const chosenState =
isChosen && isCorrect
? 'correct'
: isChosen
? 'incorrect'
: isCorrect
? 'answer'
: 'default';
return (
<ChoiceItem key={choice.number} $correct={isCorrect}>
<ChoiceNumber $correct={isCorrect}>{choice.number}</ChoiceNumber>
<ChoiceItem key={choice.number} $state={chosenState}>
<ChoiceNumber $state={chosenState}>{choice.number}</ChoiceNumber>
<ChoiceText>{choice.text}</ChoiceText>
{isChosen ? (
<ChoiceBadge $tone={isCorrect ? 'correct' : 'incorrect'}>
<Icon
name={isCorrect ? 'check-circle' : 'x'}
weight="fill"
size={14}
/>
</ChoiceBadge>
) : null}
{isCorrect ? (
<ChoiceBadge>
<Icon name="check-circle" weight="fill" size={14} />
@@ -169,9 +195,6 @@ function StudyLogDetailBody() {
})}
</ChoiceList>
)}
<InlineNote>
DB에 .
</InlineNote>
</SectionCard>
<SectionCard>
@@ -429,19 +452,33 @@ const ChoiceList = styled.div`
margin-top: 20px;
`;
const ChoiceItem = styled.div<{ $correct: boolean }>`
type ChoiceState = 'default' | 'answer' | 'correct' | 'incorrect';
const ChoiceItem = styled.div<{ $state: ChoiceState }>`
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid
${({ $correct }) => ($correct ? 'rgba(16, 185, 129, 0.4)' : theme.color.borderSoftAlpha)};
background: ${({ $correct }) =>
$correct ? 'rgba(16, 185, 129, 0.12)' : 'rgba(255, 255, 255, 0.02)'};
border: 1px solid ${({ $state }) => {
if ($state === 'correct' || $state === 'answer') return 'rgba(16, 185, 129, 0.4)';
if ($state === 'incorrect') return 'rgba(244, 63, 94, 0.45)';
return theme.color.borderSoftAlpha;
}};
background: ${({ $state }) => {
if ($state === 'correct' || $state === 'answer') return 'rgba(16, 185, 129, 0.12)';
if ($state === 'incorrect') return 'rgba(244, 63, 94, 0.1)';
return 'rgba(255, 255, 255, 0.02)';
}};
box-shadow: ${({ $state }) => {
if ($state === 'correct') return '0 0 0 2px rgba(16, 185, 129, 0.28) inset';
if ($state === 'incorrect') return '0 0 0 2px rgba(244, 63, 94, 0.24) inset';
if ($state === 'answer') return '0 0 0 2px rgba(16, 185, 129, 0.24) inset';
return 'none';
}};
`;
const ChoiceNumber = styled.div<{ $correct: boolean }>`
const ChoiceNumber = styled.div<{ $state: ChoiceState }>`
width: 34px;
height: 34px;
display: inline-flex;
@@ -449,8 +486,22 @@ const ChoiceNumber = styled.div<{ $correct: boolean }>`
justify-content: center;
border-radius: 999px;
font-weight: 700;
color: ${({ $correct }) => ($correct ? '#a7f3d0' : theme.color.textBright)};
background: ${({ $correct }) => ($correct ? 'rgba(5, 150, 105, 0.38)' : theme.color.surface2)};
color: ${({ $state }) => {
if ($state === 'correct' || $state === 'answer') return '#a7f3d0';
if ($state === 'incorrect') return '#fecdd3';
return theme.color.textBright;
}};
background: ${({ $state }) => {
if ($state === 'correct' || $state === 'answer') return 'rgba(5, 150, 105, 0.38)';
if ($state === 'incorrect') return 'rgba(190, 24, 93, 0.36)';
return theme.color.surface2;
}};
box-shadow: ${({ $state }) => {
if ($state === 'correct') return '0 0 0 3px rgba(16, 185, 129, 0.22)';
if ($state === 'incorrect') return '0 0 0 3px rgba(99, 102, 241, 0.34)';
if ($state === 'answer') return '0 0 0 3px rgba(16, 185, 129, 0.2)';
return 'none';
}};
`;
const ChoiceText = styled.div`
@@ -459,22 +510,15 @@ const ChoiceText = styled.div`
line-height: 1.6;
`;
const ChoiceBadge = styled.div`
const ChoiceBadge = styled.div<{ $tone?: 'correct' | 'incorrect' }>`
display: inline-flex;
align-items: center;
gap: 6px;
color: #86efac;
color: ${({ $tone }) => ($tone === 'incorrect' ? '#fda4af' : '#86efac')};
font-size: 12px;
font-weight: 700;
`;
const InlineNote = styled.p`
margin: 16px 0 0;
color: ${theme.color.textMute};
font-size: 12px;
line-height: 1.6;
`;
const MemoArea = styled(Textarea)`
min-height: 140px;
`;

View File

@@ -96,7 +96,6 @@ export default function SideNav({ user, reviewCount }: SideNavProps) {
const MENU_ITEMS = [
{ label: '프로필', href: '/profile', icon: 'user' as const },
{ label: '설정', href: '/profile#settings', icon: 'gear' as const },
];
return (

View File

@@ -133,6 +133,7 @@ export interface StudyLog {
difficulty: number;
baseCorrectRate: number | null;
result: StudyResult;
chosenAnswer?: number | null;
memo: string | null;
studiedAt: string;
timeSpent: number | null;
@@ -153,6 +154,7 @@ export interface StudyLogDetail {
id: number;
studiedAt: string;
result: StudyResult;
chosenAnswer: number | null;
memo: string | null;
timeSpent: number | null;
difficulty: number;
@@ -304,3 +306,21 @@ export async function submitProblemSetStudy(payload: {
);
return response.data;
}
export async function getStudyLog(id: number) {
const response = await api.get<StudyLogDetail>(`/study-logs/${id}`);
return response.data;
}
export async function updateStudyLog(
id: number,
payload: Partial<Pick<StudyLogDetail, 'chosenAnswer' | 'result' | 'memo'>>,
) {
const response = await api.patch<{
id: number;
chosenAnswer: number | null;
result: StudyResult;
memo: string | null;
}>(`/study-logs/${id}`, payload);
return response.data;
}