diff --git a/prisma/seed-test-his.js b/prisma/seed-test-his.js new file mode 100644 index 0000000..c0e8665 --- /dev/null +++ b/prisma/seed-test-his.js @@ -0,0 +1,142 @@ +/** + * HIS-05 (오프라인 낱말) / HIS-06 (최근 사용 낱말) 테스트용 시드 + * + * 사용법: + * 컨테이너 내부: node prisma/seed-test-his.js + * USER_ID 지정: USER_ID=xxxx node prisma/seed-test-his.js + */ + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + // ── 0. 대상 유저 결정 ────────────────────────────────── + let userId = process.env.USER_ID; + + if (!userId) { + const user = await prisma.user.findFirst({ orderBy: { createdAt: 'asc' } }); + if (!user) throw new Error('DB에 유저가 없습니다. 먼저 회원가입하세요.'); + userId = user.id; + } + + console.log(`🎯 대상 유저 ID: ${userId}\n`); + + // ── 1. 기존 테스트 데이터 정리 ──────────────────────── + await prisma.conversationHistory.deleteMany({ where: { userId } }); + await prisma.userWord.deleteMany({ where: { userId } }); + await prisma.userCategory.deleteMany({ where: { userId } }); + console.log('🗑️ 기존 데이터 삭제 완료'); + + // ── 2. UserCategory 생성 ────────────────────────────── + const category = await prisma.userCategory.create({ + data: { + userId, + categoryName: '일상', + displayOrder: 1, + }, + }); + console.log(`📁 카테고리 생성: ${category.categoryName}`); + + // ── 3. UserWord 생성 (isFavorite 포함) ──────────────── + const favoriteWords = ['밥', '물', '화장실', '좋아', '싫어']; + const normalWords = ['학교', '집', '엄마', '아빠', '친구', '선생님', '버스']; + + let order = 1; + + for (const word of favoriteWords) { + await prisma.userWord.create({ + data: { + userId, + userCategoryId: category.id, + partOfSpeech: 'NOUN', + customWord: word, + displayOrder: order++, + isFavorite: true, + isDeleted: false, + }, + }); + } + + for (const word of normalWords) { + await prisma.userWord.create({ + data: { + userId, + userCategoryId: category.id, + partOfSpeech: 'NOUN', + customWord: word, + displayOrder: order++, + isFavorite: false, + isDeleted: false, + }, + }); + } + + console.log(`✨ UserWord 생성: 즐겨찾기 ${favoriteWords.length}개, 일반 ${normalWords.length}개`); + + // ── 4. ConversationHistory 생성 ─────────────────────── + const now = new Date(); + + const daysAgo = (d) => { + const date = new Date(now); + date.setDate(date.getDate() - d); + return date; + }; + + // inputWords 형식: [{word, order}] + const makeInputWords = (...words) => + words.map((w, i) => ({ word: w, order: i })); + + const histories = [ + // 최근 1주일 이내 (HIS-06 recent-words) + { days: 0, words: ['밥', '먹다'], sentence: '밥 먹을게요.' }, + { days: 1, words: ['물', '주다'], sentence: '물 주세요.' }, + { days: 2, words: ['화장실', '가다'], sentence: '화장실 가고 싶어요.' }, + { days: 3, words: ['좋아', '이거'], sentence: '이거 좋아요.' }, + { days: 4, words: ['엄마', '보고싶다'], sentence: '엄마 보고 싶어요.' }, + { days: 5, words: ['밥', '주다'], sentence: '밥 주세요.' }, + { days: 6, words: ['집', '가다'], sentence: '집에 가고 싶어요.' }, + + // 최근 3개월 이내 (HIS-05 offline-words 빈도 계산용) + { days: 10, words: ['밥', '먹다'], sentence: '밥 먹었어요.' }, + { days: 15, words: ['밥', '주다'], sentence: '밥 주세요.' }, + { days: 20, words: ['물', '마시다'], sentence: '물 마실게요.' }, + { days: 25, words: ['화장실', '급하다'], sentence: '화장실이 급해요.' }, + { days: 30, words: ['학교', '가다'], sentence: '학교 가요.' }, + { days: 40, words: ['친구', '만나다'], sentence: '친구 만나요.' }, + { days: 50, words: ['밥', '싫어'], sentence: '밥 싫어요.' }, + { days: 60, words: ['선생님', '안녕'], sentence: '선생님 안녕하세요.' }, + { days: 70, words: ['버스', '타다'], sentence: '버스 타요.' }, + { days: 80, words: ['물', '주다'], sentence: '물 주세요.' }, + ]; + + for (const h of histories) { + await prisma.conversationHistory.create({ + data: { + userId, + inputWords: makeInputWords(...h.words), + inputType: 'WORD_ONLY', + suggestedSentences: [h.sentence], + selectedSentence: h.sentence, + isOutputted: true, + isDeleted: false, + createdAt: daysAgo(h.days), + }, + }); + } + + console.log(`📝 ConversationHistory 생성: ${histories.length}개`); + console.log('\n✅ 시드 완료!'); + console.log('\n테스트:'); + console.log(' HIS-05 → GET /api/histories/offline-words?limit=30'); + console.log(' HIS-06 → GET /api/histories/recent-words'); +} + +main() + .catch((e) => { + console.error('❌ Seed error:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/history/history.repository.js b/src/history/history.repository.js index 656bb84..cfa05bc 100644 --- a/src/history/history.repository.js +++ b/src/history/history.repository.js @@ -101,14 +101,14 @@ export const findFrequentWords = async (userId, frequentLimit = 80) => { // (1) 즐겨찾기 단어들을 먼저 Map에 담기 (isFavorite: true) favorites.forEach(fav => { - const key = fav.wordId || fav.customWord; + const key = fav.customWord; detailMap.set(key, { - wordId: fav.wordId || null, - word: fav.customWord || fav.word?.word, - imageUrl: fav.customImageUrl || fav.word?.imageUrl, - categoryId: fav.category?.id || null, - categoryName: fav.category?.categoryName, - isFavorite: true, // 즐겨찾기 여부 명시 + wordId: null, + word: fav.customWord, + imageUrl: fav.customImageUrl, + categoryId: fav.userCategory?.id || null, + categoryName: fav.userCategory?.categoryName, + isFavorite: true, usageCount: wordFrequency.get(key)?.count || 0, lastUsedAt: wordFrequency.get(key)?.lastUsedAt || null }); @@ -120,39 +120,35 @@ export const findFrequentWords = async (userId, frequentLimit = 80) => { const [dbWords, dbUserWords, dbWordsByText] = await Promise.all([ prisma.word.findMany({ where: { id: { in: wordIds } }, include: { category: true } }), - // ID 기반 조회와 더불어, 텍스트(customWord) 기반 조회도 추가하여 누락 방지 - prisma.userWord.findMany({ - where: { + prisma.userWord.findMany({ + where: { userId, - OR: [ - { wordId: { in: wordIds } }, - { customWord: { in: wordTexts } } - ], - isDeleted: false - }, - include: { category: true, word: true } + customWord: { in: wordTexts }, + isDeleted: false + }, + include: { userCategory: true } }), prisma.word.findMany({ where: { word: { in: wordTexts } }, include: { category: true } }) ]); - // 병합 로직에서 wordId뿐만 아니라 customWord(텍스트)도 키값으로 활용하도록 보완 + // customWord 기반 UserWord 병합 dbUserWords.forEach(uw => { - const key = uw.wordId || uw.customWord; + const key = uw.customWord; if (!detailMap.has(key)) { detailMap.set(key, { - wordId: uw.wordId || null, - word: uw.customWord || uw.word?.word, - imageUrl: uw.customImageUrl || uw.word?.imageUrl, - categoryId: uw.category?.id || null, - categoryName: uw.category?.categoryName, - isFavorite: uw.isFavorite, // DB 상태 반영 + wordId: null, + word: uw.customWord, + imageUrl: uw.customImageUrl, + categoryId: uw.userCategory?.id || null, + categoryName: uw.userCategory?.categoryName, + isFavorite: uw.isFavorite, usageCount: wordFrequency.get(key)?.count || 0, lastUsedAt: wordFrequency.get(key)?.lastUsedAt || null }); } }); - // 중복 제거하며 Map 채우기 (이미 즐겨찾기로 들어간 건 건너뜀) + // wordId 기반 Word 병합 (기존 대화 이력에 wordId가 저장된 경우) dbWords.forEach(w => { if (!detailMap.has(w.id)) { detailMap.set(w.id, { @@ -168,17 +164,18 @@ export const findFrequentWords = async (userId, frequentLimit = 80) => { } }); - dbUserWords.forEach(uw => { - if (!detailMap.has(uw.wordId)) { - detailMap.set(uw.wordId, { - wordId: uw.wordId || null, - word: uw.customWord || uw.word?.word, - imageUrl: uw.customImageUrl || uw.word?.imageUrl, - categoryId: uw.category?.id || null, - categoryName: uw.category?.categoryName, + // 텍스트 기반 Word 병합 + dbWordsByText.forEach(w => { + if (!detailMap.has(w.word)) { + detailMap.set(w.word, { + wordId: w.id, + word: w.word, + imageUrl: w.imageUrl, + categoryId: w.category?.id || null, + categoryName: w.category?.categoryName, isFavorite: false, - usageCount: wordFrequency.get(uw.wordId)?.count || 0, - lastUsedAt: wordFrequency.get(uw.wordId)?.lastUsedAt || null + usageCount: wordFrequency.get(w.word)?.count || 0, + lastUsedAt: wordFrequency.get(w.word)?.lastUsedAt || null }); } });