Skip to content

Commit 246f9fe

Browse files
authored
[Fix] HIS-05 UserWord 스키마 변경 미반영 쿼리 오류 수정 (#104)
2 parents 8a3642c + 8f305fe commit 246f9fe

File tree

2 files changed

+175
-36
lines changed

2 files changed

+175
-36
lines changed

prisma/seed-test-his.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* HIS-05 (오프라인 낱말) / HIS-06 (최근 사용 낱말) 테스트용 시드
3+
*
4+
* 사용법:
5+
* 컨테이너 내부: node prisma/seed-test-his.js
6+
* USER_ID 지정: USER_ID=xxxx node prisma/seed-test-his.js
7+
*/
8+
9+
import { PrismaClient } from '@prisma/client';
10+
11+
const prisma = new PrismaClient();
12+
13+
async function main() {
14+
// ── 0. 대상 유저 결정 ──────────────────────────────────
15+
let userId = process.env.USER_ID;
16+
17+
if (!userId) {
18+
const user = await prisma.user.findFirst({ orderBy: { createdAt: 'asc' } });
19+
if (!user) throw new Error('DB에 유저가 없습니다. 먼저 회원가입하세요.');
20+
userId = user.id;
21+
}
22+
23+
console.log(`🎯 대상 유저 ID: ${userId}\n`);
24+
25+
// ── 1. 기존 테스트 데이터 정리 ────────────────────────
26+
await prisma.conversationHistory.deleteMany({ where: { userId } });
27+
await prisma.userWord.deleteMany({ where: { userId } });
28+
await prisma.userCategory.deleteMany({ where: { userId } });
29+
console.log('🗑️ 기존 데이터 삭제 완료');
30+
31+
// ── 2. UserCategory 생성 ──────────────────────────────
32+
const category = await prisma.userCategory.create({
33+
data: {
34+
userId,
35+
categoryName: '일상',
36+
displayOrder: 1,
37+
},
38+
});
39+
console.log(`📁 카테고리 생성: ${category.categoryName}`);
40+
41+
// ── 3. UserWord 생성 (isFavorite 포함) ────────────────
42+
const favoriteWords = ['밥', '물', '화장실', '좋아', '싫어'];
43+
const normalWords = ['학교', '집', '엄마', '아빠', '친구', '선생님', '버스'];
44+
45+
let order = 1;
46+
47+
for (const word of favoriteWords) {
48+
await prisma.userWord.create({
49+
data: {
50+
userId,
51+
userCategoryId: category.id,
52+
partOfSpeech: 'NOUN',
53+
customWord: word,
54+
displayOrder: order++,
55+
isFavorite: true,
56+
isDeleted: false,
57+
},
58+
});
59+
}
60+
61+
for (const word of normalWords) {
62+
await prisma.userWord.create({
63+
data: {
64+
userId,
65+
userCategoryId: category.id,
66+
partOfSpeech: 'NOUN',
67+
customWord: word,
68+
displayOrder: order++,
69+
isFavorite: false,
70+
isDeleted: false,
71+
},
72+
});
73+
}
74+
75+
console.log(`✨ UserWord 생성: 즐겨찾기 ${favoriteWords.length}개, 일반 ${normalWords.length}개`);
76+
77+
// ── 4. ConversationHistory 생성 ───────────────────────
78+
const now = new Date();
79+
80+
const daysAgo = (d) => {
81+
const date = new Date(now);
82+
date.setDate(date.getDate() - d);
83+
return date;
84+
};
85+
86+
// inputWords 형식: [{word, order}]
87+
const makeInputWords = (...words) =>
88+
words.map((w, i) => ({ word: w, order: i }));
89+
90+
const histories = [
91+
// 최근 1주일 이내 (HIS-06 recent-words)
92+
{ days: 0, words: ['밥', '먹다'], sentence: '밥 먹을게요.' },
93+
{ days: 1, words: ['물', '주다'], sentence: '물 주세요.' },
94+
{ days: 2, words: ['화장실', '가다'], sentence: '화장실 가고 싶어요.' },
95+
{ days: 3, words: ['좋아', '이거'], sentence: '이거 좋아요.' },
96+
{ days: 4, words: ['엄마', '보고싶다'], sentence: '엄마 보고 싶어요.' },
97+
{ days: 5, words: ['밥', '주다'], sentence: '밥 주세요.' },
98+
{ days: 6, words: ['집', '가다'], sentence: '집에 가고 싶어요.' },
99+
100+
// 최근 3개월 이내 (HIS-05 offline-words 빈도 계산용)
101+
{ days: 10, words: ['밥', '먹다'], sentence: '밥 먹었어요.' },
102+
{ days: 15, words: ['밥', '주다'], sentence: '밥 주세요.' },
103+
{ days: 20, words: ['물', '마시다'], sentence: '물 마실게요.' },
104+
{ days: 25, words: ['화장실', '급하다'], sentence: '화장실이 급해요.' },
105+
{ days: 30, words: ['학교', '가다'], sentence: '학교 가요.' },
106+
{ days: 40, words: ['친구', '만나다'], sentence: '친구 만나요.' },
107+
{ days: 50, words: ['밥', '싫어'], sentence: '밥 싫어요.' },
108+
{ days: 60, words: ['선생님', '안녕'], sentence: '선생님 안녕하세요.' },
109+
{ days: 70, words: ['버스', '타다'], sentence: '버스 타요.' },
110+
{ days: 80, words: ['물', '주다'], sentence: '물 주세요.' },
111+
];
112+
113+
for (const h of histories) {
114+
await prisma.conversationHistory.create({
115+
data: {
116+
userId,
117+
inputWords: makeInputWords(...h.words),
118+
inputType: 'WORD_ONLY',
119+
suggestedSentences: [h.sentence],
120+
selectedSentence: h.sentence,
121+
isOutputted: true,
122+
isDeleted: false,
123+
createdAt: daysAgo(h.days),
124+
},
125+
});
126+
}
127+
128+
console.log(`📝 ConversationHistory 생성: ${histories.length}개`);
129+
console.log('\n✅ 시드 완료!');
130+
console.log('\n테스트:');
131+
console.log(' HIS-05 → GET /api/histories/offline-words?limit=30');
132+
console.log(' HIS-06 → GET /api/histories/recent-words');
133+
}
134+
135+
main()
136+
.catch((e) => {
137+
console.error('❌ Seed error:', e);
138+
process.exit(1);
139+
})
140+
.finally(async () => {
141+
await prisma.$disconnect();
142+
});

src/history/history.repository.js

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,14 @@ export const findFrequentWords = async (userId, frequentLimit = 80) => {
101101

102102
// (1) 즐겨찾기 단어들을 먼저 Map에 담기 (isFavorite: true)
103103
favorites.forEach(fav => {
104-
const key = fav.wordId || fav.customWord;
104+
const key = fav.customWord;
105105
detailMap.set(key, {
106-
wordId: fav.wordId || null,
107-
word: fav.customWord || fav.word?.word,
108-
imageUrl: fav.customImageUrl || fav.word?.imageUrl,
109-
categoryId: fav.category?.id || null,
110-
categoryName: fav.category?.categoryName,
111-
isFavorite: true, // 즐겨찾기 여부 명시
106+
wordId: null,
107+
word: fav.customWord,
108+
imageUrl: fav.customImageUrl,
109+
categoryId: fav.userCategory?.id || null,
110+
categoryName: fav.userCategory?.categoryName,
111+
isFavorite: true,
112112
usageCount: wordFrequency.get(key)?.count || 0,
113113
lastUsedAt: wordFrequency.get(key)?.lastUsedAt || null
114114
});
@@ -120,39 +120,35 @@ export const findFrequentWords = async (userId, frequentLimit = 80) => {
120120

121121
const [dbWords, dbUserWords, dbWordsByText] = await Promise.all([
122122
prisma.word.findMany({ where: { id: { in: wordIds } }, include: { category: true } }),
123-
// ID 기반 조회와 더불어, 텍스트(customWord) 기반 조회도 추가하여 누락 방지
124-
prisma.userWord.findMany({
125-
where: {
123+
prisma.userWord.findMany({
124+
where: {
126125
userId,
127-
OR: [
128-
{ wordId: { in: wordIds } },
129-
{ customWord: { in: wordTexts } }
130-
],
131-
isDeleted: false
132-
},
133-
include: { category: true, word: true }
126+
customWord: { in: wordTexts },
127+
isDeleted: false
128+
},
129+
include: { userCategory: true }
134130
}),
135131
prisma.word.findMany({ where: { word: { in: wordTexts } }, include: { category: true } })
136132
]);
137133

138-
// 병합 로직에서 wordId뿐만 아니라 customWord(텍스트)도 키값으로 활용하도록 보완
134+
// customWord 기반 UserWord 병합
139135
dbUserWords.forEach(uw => {
140-
const key = uw.wordId || uw.customWord;
136+
const key = uw.customWord;
141137
if (!detailMap.has(key)) {
142138
detailMap.set(key, {
143-
wordId: uw.wordId || null,
144-
word: uw.customWord || uw.word?.word,
145-
imageUrl: uw.customImageUrl || uw.word?.imageUrl,
146-
categoryId: uw.category?.id || null,
147-
categoryName: uw.category?.categoryName,
148-
isFavorite: uw.isFavorite, // DB 상태 반영
139+
wordId: null,
140+
word: uw.customWord,
141+
imageUrl: uw.customImageUrl,
142+
categoryId: uw.userCategory?.id || null,
143+
categoryName: uw.userCategory?.categoryName,
144+
isFavorite: uw.isFavorite,
149145
usageCount: wordFrequency.get(key)?.count || 0,
150146
lastUsedAt: wordFrequency.get(key)?.lastUsedAt || null
151147
});
152148
}
153149
});
154150

155-
// 중복 제거하며 Map 채우기 (이미 즐겨찾기로 들어간 건 건너뜀)
151+
// wordId 기반 Word 병합 (기존 대화 이력에 wordId가 저장된 경우)
156152
dbWords.forEach(w => {
157153
if (!detailMap.has(w.id)) {
158154
detailMap.set(w.id, {
@@ -168,17 +164,18 @@ export const findFrequentWords = async (userId, frequentLimit = 80) => {
168164
}
169165
});
170166

171-
dbUserWords.forEach(uw => {
172-
if (!detailMap.has(uw.wordId)) {
173-
detailMap.set(uw.wordId, {
174-
wordId: uw.wordId || null,
175-
word: uw.customWord || uw.word?.word,
176-
imageUrl: uw.customImageUrl || uw.word?.imageUrl,
177-
categoryId: uw.category?.id || null,
178-
categoryName: uw.category?.categoryName,
167+
// 텍스트 기반 Word 병합
168+
dbWordsByText.forEach(w => {
169+
if (!detailMap.has(w.word)) {
170+
detailMap.set(w.word, {
171+
wordId: w.id,
172+
word: w.word,
173+
imageUrl: w.imageUrl,
174+
categoryId: w.category?.id || null,
175+
categoryName: w.category?.categoryName,
179176
isFavorite: false,
180-
usageCount: wordFrequency.get(uw.wordId)?.count || 0,
181-
lastUsedAt: wordFrequency.get(uw.wordId)?.lastUsedAt || null
177+
usageCount: wordFrequency.get(w.word)?.count || 0,
178+
lastUsedAt: wordFrequency.get(w.word)?.lastUsedAt || null
182179
});
183180
}
184181
});

0 commit comments

Comments
 (0)