Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions prisma/seed-test-his.js
Original file line number Diff line number Diff line change
@@ -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();
});
69 changes: 33 additions & 36 deletions src/history/history.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand All @@ -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, {
Expand All @@ -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
});
}
});
Expand Down