feat: add group recipe sharing and translation
This commit is contained in:
@@ -55,10 +55,12 @@ export async function buildApp({ config = loadConfig(), dependencies = {} } = {}
|
||||
|
||||
if (
|
||||
dependencies.allowedEmailRepository
|
||||
&& dependencies.groupRepository
|
||||
&& dependencies.recipeRepository
|
||||
&& dependencies.userRepository
|
||||
) {
|
||||
app.decorate('allowedEmailRepository', dependencies.allowedEmailRepository);
|
||||
app.decorate('groupRepository', dependencies.groupRepository);
|
||||
app.decorate('recipeRepository', dependencies.recipeRepository);
|
||||
app.decorate('userRepository', dependencies.userRepository);
|
||||
} else {
|
||||
|
||||
@@ -37,6 +37,7 @@ export function mongoCollectionNames(nodeEnv) {
|
||||
const suffix = nodeEnv === 'development' ? '_dev' : nodeEnv === 'test' ? '_test' : '';
|
||||
return Object.freeze({
|
||||
allowedEmails: 'allowed_google_emails',
|
||||
groups: `groups${suffix}`,
|
||||
recipes: `recipes${suffix}`,
|
||||
users: `users${suffix}`,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { AllowedEmailRepository } from '../repositories/allowed-email.repository.js';
|
||||
import { GroupRepository } from '../repositories/group.repository.js';
|
||||
import { RecipeRepository } from '../repositories/recipe.repository.js';
|
||||
import { UserRepository } from '../repositories/user.repository.js';
|
||||
|
||||
@@ -49,11 +50,13 @@ async function mongoPlugin(fastify, { config }) {
|
||||
const db = client.db();
|
||||
const collections = config.mongoCollections;
|
||||
const allowedEmailRepository = new AllowedEmailRepository(db, collections.allowedEmails);
|
||||
const groupRepository = new GroupRepository(db, collections.groups);
|
||||
const recipeRepository = new RecipeRepository(db, collections.recipes);
|
||||
const userRepository = new UserRepository(db, collections.users);
|
||||
|
||||
await Promise.all([
|
||||
allowedEmailRepository.ensureIndexes(),
|
||||
groupRepository.ensureIndexes(),
|
||||
recipeRepository.ensureIndexes(),
|
||||
userRepository.ensureIndexes(),
|
||||
]);
|
||||
@@ -65,6 +68,7 @@ async function mongoPlugin(fastify, { config }) {
|
||||
fastify.decorate('mongoClient', client);
|
||||
fastify.decorate('db', db);
|
||||
fastify.decorate('allowedEmailRepository', allowedEmailRepository);
|
||||
fastify.decorate('groupRepository', groupRepository);
|
||||
fastify.decorate('recipeRepository', recipeRepository);
|
||||
fastify.decorate('userRepository', userRepository);
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export class GroupRepository {
|
||||
constructor(db, collectionName = 'groups') {
|
||||
this.collection = db.collection(collectionName);
|
||||
}
|
||||
|
||||
async ensureIndexes() {
|
||||
await this.collection.createIndex({ memberGoogleSubs: 1 });
|
||||
}
|
||||
|
||||
async listMemberGoogleSubs(googleSub) {
|
||||
const groups = await this.collection
|
||||
.find(
|
||||
{ memberGoogleSubs: googleSub },
|
||||
{ projection: { _id: 0, memberGoogleSubs: 1 } },
|
||||
)
|
||||
.toArray();
|
||||
|
||||
return [...new Set(
|
||||
groups
|
||||
.flatMap((group) => group.memberGoogleSubs ?? [])
|
||||
.filter((memberGoogleSub) => typeof memberGoogleSub === 'string'),
|
||||
)];
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,13 @@ export class RecipeRepository {
|
||||
}
|
||||
|
||||
async listByOwner(ownerGoogleSub) {
|
||||
return this.listByOwners([ownerGoogleSub]);
|
||||
}
|
||||
|
||||
async listByOwners(ownerGoogleSubs) {
|
||||
return this.collection
|
||||
.find(
|
||||
{ ownerGoogleSub },
|
||||
{ ownerGoogleSub: { $in: ownerGoogleSubs } },
|
||||
{
|
||||
projection: {
|
||||
'source.rawText': 0,
|
||||
@@ -34,6 +38,13 @@ export class RecipeRepository {
|
||||
return this.collection.findOne({ _id: recipeId, ownerGoogleSub });
|
||||
}
|
||||
|
||||
async findByIdForOwners(ownerGoogleSubs, recipeId) {
|
||||
return this.collection.findOne({
|
||||
_id: recipeId,
|
||||
ownerGoogleSub: { $in: ownerGoogleSubs },
|
||||
});
|
||||
}
|
||||
|
||||
async findBySource(ownerGoogleSub, platform, sourceId) {
|
||||
return this.collection.findOne({
|
||||
ownerGoogleSub,
|
||||
|
||||
@@ -29,13 +29,23 @@ function verifiedSource(source) {
|
||||
return source;
|
||||
}
|
||||
|
||||
async function visibleOwnerGoogleSubs(fastify, googleSub) {
|
||||
const groupMembers = await fastify.groupRepository.listMemberGoogleSubs(googleSub);
|
||||
return [...new Set([googleSub, ...groupMembers])];
|
||||
}
|
||||
|
||||
export default async function recipeRoutes(fastify) {
|
||||
fastify.get('/api/recipes', { preHandler: fastify.authenticate }, async (request) => ({
|
||||
recipes: await fastify.recipeRepository.listByOwner(request.user.sub),
|
||||
}));
|
||||
fastify.get('/api/recipes', { preHandler: fastify.authenticate }, async (request) => {
|
||||
const ownerGoogleSubs = await visibleOwnerGoogleSubs(fastify, request.user.sub);
|
||||
return { recipes: await fastify.recipeRepository.listByOwners(ownerGoogleSubs) };
|
||||
});
|
||||
|
||||
fastify.get('/api/recipes/:id', { preHandler: fastify.authenticate }, async (request) => {
|
||||
const recipe = await fastify.recipeRepository.findById(request.user.sub, request.params.id);
|
||||
const ownerGoogleSubs = await visibleOwnerGoogleSubs(fastify, request.user.sub);
|
||||
const recipe = await fastify.recipeRepository.findByIdForOwners(
|
||||
ownerGoogleSubs,
|
||||
request.params.id,
|
||||
);
|
||||
if (!recipe) throw new NotFoundError('레시피를 찾을 수 없습니다.');
|
||||
return { recipe };
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@ const SYSTEM_PROMPT = `당신은 원문에서 레시피를 구조화하는 데
|
||||
- 수량이 명확하지 않으면 null 또는 원문 표현을 유지합니다.
|
||||
- g, ml, 큰술, 작은술, 장, 개 등의 원문 단위를 유지합니다.
|
||||
- 광고, 비즈니스 문의, SNS 링크, 해시태그 등 레시피와 무관한 내용을 제거합니다.
|
||||
- 원문이 주로 영어 또는 다른 외국어이면 title, summary, servings, ingredientGroups, steps, tips를 자연스러운 한국어로 번역합니다.
|
||||
- 번역할 때 재료와 요리의 고유명사는 의미가 달라지지 않게 유지하고, 수량, 온도, 시간 값은 변환하지 않습니다.
|
||||
- 원문이 이미 한국어이면 불필요하게 문체나 표현을 바꾸지 않습니다.
|
||||
- YouTube timestamp는 원문 transcript에서 확인되는 경우에만 초 단위 숫자로 기록합니다.
|
||||
- Instagram 단계의 timestampSec은 null입니다.
|
||||
- 원문에 재료 그룹 이름이 없으면 "재료"를 사용합니다. ingredientGroups[].name은 null이 될 수 없습니다.
|
||||
|
||||
Reference in New Issue
Block a user