734 lines
21 KiB
JavaScript
734 lines
21 KiB
JavaScript
const { v4: uuidv4 } = require('uuid');
|
|
const mysql = require('mysql2/promise');
|
|
|
|
const CATEGORY_SET = new Set(['KOREAN', 'CHINESE', 'JAPANESE', 'WESTERN', 'OTHER']);
|
|
|
|
const seedRestaurants = [
|
|
{
|
|
id: 'a1f5d3a5-1c39-4c7d-a0fb-2f8f8f5f7a11',
|
|
name: '강남스시마루',
|
|
category: 'JAPANESE',
|
|
address: '서울 강남구 테헤란로 123',
|
|
phone: '02-1234-5678',
|
|
lat: 37.5009,
|
|
lng: 127.0368,
|
|
},
|
|
{
|
|
id: '2f9b0c77-dcf7-44c5-8fcb-aefb4a16c222',
|
|
name: '북경반점 선릉점',
|
|
category: 'CHINESE',
|
|
address: '서울 강남구 선릉로 88',
|
|
phone: '02-7890-1234',
|
|
lat: 37.5043,
|
|
lng: 127.0496,
|
|
},
|
|
{
|
|
id: '3be86df0-8b69-4f1b-9f0f-c08726f78333',
|
|
name: '파스타하우스 역삼',
|
|
category: 'WESTERN',
|
|
address: '서울 강남구 역삼로 201',
|
|
phone: '02-2222-3333',
|
|
lat: 37.4982,
|
|
lng: 127.0397,
|
|
},
|
|
{
|
|
id: '4d645f7f-0f08-4cb2-b218-0dd07a838444',
|
|
name: '한그릇집 역삼점',
|
|
category: 'KOREAN',
|
|
address: '서울 강남구 논현로 401',
|
|
phone: '02-5566-7788',
|
|
lat: 37.5012,
|
|
lng: 127.0301,
|
|
},
|
|
];
|
|
|
|
const seedUsers = [
|
|
{ id: '77a341ce-68b4-4fde-9f23-b64fa87cb111', providerUserId: 'demo-user-1', nickName: '나' },
|
|
{ id: 'a8d1cd2d-f4b7-4f4f-b6b6-1be63e2f8222', providerUserId: 'user-a', nickName: '점심러버' },
|
|
{ id: 'f1f4ea2a-07e9-4cb8-bd8d-a16792b96333', providerUserId: 'user-b', nickName: '직장인A' },
|
|
{ id: '4f659ac3-35f1-4907-91c7-11fe2e87a444', providerUserId: 'user-c', nickName: '직장인C' },
|
|
{ id: '555f6868-c2b5-4025-b0cb-f2f65f1bc555', providerUserId: 'user-d', nickName: '직장인D' },
|
|
];
|
|
|
|
const seedReviews = [
|
|
{
|
|
id: 'r-101',
|
|
restaurantId: 'a1f5d3a5-1c39-4c7d-a0fb-2f8f8f5f7a11',
|
|
providerUserId: 'user-a',
|
|
userNickname: '점심러버',
|
|
rating: 5,
|
|
content: '초밥 신선하고 점심 세트 가성비 좋아요.',
|
|
createdAt: '2026-04-10T03:10:00Z',
|
|
},
|
|
{
|
|
id: 'r-102',
|
|
restaurantId: 'a1f5d3a5-1c39-4c7d-a0fb-2f8f8f5f7a11',
|
|
providerUserId: 'demo-user-1',
|
|
userNickname: '나',
|
|
rating: 4,
|
|
content: '웨이팅만 짧으면 자주 올 듯.',
|
|
createdAt: '2026-04-12T02:20:00Z',
|
|
},
|
|
{
|
|
id: 'r-201',
|
|
restaurantId: '2f9b0c77-dcf7-44c5-8fcb-aefb4a16c222',
|
|
providerUserId: 'user-b',
|
|
userNickname: '직장인A',
|
|
rating: 4,
|
|
content: '짬뽕 국물이 진해서 해장 점심으로 좋아요.',
|
|
createdAt: '2026-04-08T04:10:00Z',
|
|
},
|
|
];
|
|
|
|
const seedLikes = [
|
|
{ restaurantId: '2f9b0c77-dcf7-44c5-8fcb-aefb4a16c222', providerUserId: 'demo-user-1' },
|
|
{ restaurantId: 'a1f5d3a5-1c39-4c7d-a0fb-2f8f8f5f7a11', providerUserId: 'user-a' },
|
|
{ restaurantId: '3be86df0-8b69-4f1b-9f0f-c08726f78333', providerUserId: 'user-c' },
|
|
{ restaurantId: '3be86df0-8b69-4f1b-9f0f-c08726f78333', providerUserId: 'user-d' },
|
|
];
|
|
|
|
class MemoryStore {
|
|
constructor() {
|
|
this.restaurants = [...seedRestaurants];
|
|
this.users = seedUsers.map((u) => ({ ...u }));
|
|
this.reviews = seedReviews.map((r) => ({ ...r }));
|
|
this.likes = seedLikes.map((l) => ({ ...l }));
|
|
}
|
|
|
|
async getNearby({ userExternalId, lat, lng, radiusMeters, category, sort }) {
|
|
let items = this.restaurants
|
|
.map((restaurant) => this.#toRestaurantListItem(restaurant, userExternalId, lat, lng))
|
|
.filter((item) => item.distanceMeters <= radiusMeters);
|
|
|
|
if (category && CATEGORY_SET.has(category)) {
|
|
items = items.filter((item) => item.category === category);
|
|
}
|
|
|
|
return sortItems(items, sort);
|
|
}
|
|
|
|
async getRestaurantById(restaurantId, userExternalId) {
|
|
const restaurant = this.restaurants.find((item) => item.id === restaurantId);
|
|
if (!restaurant) return null;
|
|
|
|
return this.#toRestaurantListItem(
|
|
restaurant,
|
|
userExternalId,
|
|
restaurant.lat,
|
|
restaurant.lng,
|
|
);
|
|
}
|
|
|
|
async getReviews(restaurantId, userExternalId) {
|
|
return this.reviews
|
|
.filter((review) => review.restaurantId === restaurantId)
|
|
.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
|
|
.map((review) => ({
|
|
id: review.id,
|
|
restaurantId: review.restaurantId,
|
|
userExternalId: review.providerUserId,
|
|
nickName: review.userNickname,
|
|
rating: review.rating,
|
|
content: review.content,
|
|
createdAt: review.createdAt,
|
|
updatedAt: review.createdAt,
|
|
mine: review.providerUserId === userExternalId,
|
|
}));
|
|
}
|
|
|
|
async upsertReview(restaurantId, userExternalId, rating, content) {
|
|
const restaurant = this.restaurants.find((item) => item.id === restaurantId);
|
|
if (!restaurant) return null;
|
|
|
|
const user = this.#getOrCreateUser(userExternalId);
|
|
const now = new Date().toISOString();
|
|
|
|
const existing = this.reviews.find(
|
|
(review) => review.restaurantId === restaurantId && review.providerUserId === userExternalId,
|
|
);
|
|
|
|
if (existing) {
|
|
existing.rating = rating;
|
|
existing.content = content;
|
|
existing.createdAt = now;
|
|
existing.userNickname = user.nickName;
|
|
|
|
return {
|
|
id: existing.id,
|
|
restaurantId,
|
|
userExternalId,
|
|
nickName: user.nickName,
|
|
rating,
|
|
content,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
mine: true,
|
|
};
|
|
}
|
|
|
|
const created = {
|
|
id: uuidv4(),
|
|
restaurantId,
|
|
providerUserId: userExternalId,
|
|
userNickname: user.nickName,
|
|
rating,
|
|
content,
|
|
createdAt: now,
|
|
};
|
|
this.reviews.push(created);
|
|
|
|
return {
|
|
id: created.id,
|
|
restaurantId,
|
|
userExternalId,
|
|
nickName: user.nickName,
|
|
rating,
|
|
content,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
mine: true,
|
|
};
|
|
}
|
|
|
|
async deleteReview(reviewId, userExternalId) {
|
|
const index = this.reviews.findIndex((item) => item.id === reviewId);
|
|
if (index < 0) return { deleted: false, reason: 'NOT_FOUND' };
|
|
|
|
if (this.reviews[index].providerUserId !== userExternalId) {
|
|
return { deleted: false, reason: 'FORBIDDEN' };
|
|
}
|
|
|
|
this.reviews.splice(index, 1);
|
|
return { deleted: true };
|
|
}
|
|
|
|
async toggleLike(restaurantId, userExternalId) {
|
|
const restaurant = this.restaurants.find((item) => item.id === restaurantId);
|
|
if (!restaurant) return null;
|
|
|
|
const index = this.likes.findIndex(
|
|
(item) => item.restaurantId === restaurantId && item.providerUserId === userExternalId,
|
|
);
|
|
|
|
let liked = false;
|
|
if (index >= 0) {
|
|
this.likes.splice(index, 1);
|
|
} else {
|
|
this.likes.push({ restaurantId, providerUserId: userExternalId });
|
|
liked = true;
|
|
}
|
|
|
|
const likeCount = this.likes.filter((item) => item.restaurantId === restaurantId).length;
|
|
return { liked, likeCount };
|
|
}
|
|
|
|
async getMyReviews(userExternalId) {
|
|
return this.reviews
|
|
.filter((item) => item.providerUserId === userExternalId)
|
|
.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
|
|
.map((item) => ({
|
|
id: item.id,
|
|
restaurantId: item.restaurantId,
|
|
userExternalId,
|
|
nickName: item.userNickname,
|
|
rating: item.rating,
|
|
content: item.content,
|
|
createdAt: item.createdAt,
|
|
updatedAt: item.createdAt,
|
|
mine: true,
|
|
}));
|
|
}
|
|
|
|
#getOrCreateUser(userExternalId) {
|
|
const found = this.users.find((u) => u.providerUserId === userExternalId);
|
|
if (found) return found;
|
|
|
|
const created = {
|
|
id: uuidv4(),
|
|
providerUserId: userExternalId,
|
|
nickName: userExternalId === 'demo-user-1' ? '나' : '사용자',
|
|
};
|
|
this.users.push(created);
|
|
return created;
|
|
}
|
|
|
|
#toRestaurantListItem(restaurant, userExternalId, lat, lng) {
|
|
const restaurantReviews = this.reviews.filter((item) => item.restaurantId === restaurant.id);
|
|
const reviewCount = restaurantReviews.length;
|
|
const ratingSum = restaurantReviews.reduce((sum, item) => sum + item.rating, 0);
|
|
const averageRating = reviewCount === 0 ? 0 : roundTo1(ratingSum / reviewCount);
|
|
const likeCount = this.likes.filter((item) => item.restaurantId === restaurant.id).length;
|
|
|
|
return {
|
|
id: restaurant.id,
|
|
name: restaurant.name,
|
|
category: restaurant.category,
|
|
distanceMeters: Math.round(haversineMeters(lat, lng, restaurant.lat, restaurant.lng)),
|
|
averageRating,
|
|
reviewCount,
|
|
likeCount,
|
|
likedByMe: this.likes.some(
|
|
(item) => item.restaurantId === restaurant.id && item.providerUserId === userExternalId,
|
|
),
|
|
address: restaurant.address,
|
|
phone: restaurant.phone,
|
|
lat: restaurant.lat,
|
|
lng: restaurant.lng,
|
|
};
|
|
}
|
|
}
|
|
|
|
class MySQLStore {
|
|
constructor(pool) {
|
|
this.pool = pool;
|
|
}
|
|
|
|
async bootstrap() {
|
|
const [rows] = await this.pool.query('select count(*) as cnt from restaurant');
|
|
if ((rows[0]?.cnt ?? 0) > 0) {
|
|
return;
|
|
}
|
|
|
|
for (const user of seedUsers) {
|
|
await this.pool.query(
|
|
`insert into app_user (id, provider, provider_user_id, nick_name)
|
|
values (?, 'toss', ?, ?)
|
|
on duplicate key update nick_name = values(nick_name)`,
|
|
[user.id, user.providerUserId, user.nickName],
|
|
);
|
|
}
|
|
|
|
for (const restaurant of seedRestaurants) {
|
|
await this.pool.query(
|
|
`insert into restaurant (id, external_place_id, name, category, address, phone, lat, lng, is_active)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
on duplicate key update
|
|
name = values(name),
|
|
category = values(category),
|
|
address = values(address),
|
|
phone = values(phone),
|
|
lat = values(lat),
|
|
lng = values(lng),
|
|
is_active = 1`,
|
|
[
|
|
restaurant.id,
|
|
`seed-${restaurant.id}`,
|
|
restaurant.name,
|
|
restaurant.category,
|
|
restaurant.address,
|
|
restaurant.phone,
|
|
restaurant.lat,
|
|
restaurant.lng,
|
|
],
|
|
);
|
|
}
|
|
|
|
for (const review of seedReviews) {
|
|
const user = seedUsers.find((u) => u.providerUserId === review.providerUserId);
|
|
if (!user) continue;
|
|
|
|
await this.pool.query(
|
|
`insert into restaurant_review (id, restaurant_id, user_id, rating, content, created_at, updated_at, deleted_at)
|
|
values (?, ?, ?, ?, ?, ?, ?, null)
|
|
on duplicate key update
|
|
rating = values(rating),
|
|
content = values(content),
|
|
updated_at = values(updated_at),
|
|
deleted_at = null`,
|
|
[
|
|
review.id,
|
|
review.restaurantId,
|
|
user.id,
|
|
review.rating,
|
|
review.content,
|
|
review.createdAt,
|
|
review.createdAt,
|
|
],
|
|
);
|
|
await this.#refreshStat(review.restaurantId);
|
|
}
|
|
|
|
for (const like of seedLikes) {
|
|
const user = seedUsers.find((u) => u.providerUserId === like.providerUserId);
|
|
if (!user) continue;
|
|
await this.pool.query(
|
|
`insert ignore into restaurant_like (restaurant_id, user_id) values (?, ?)`,
|
|
[like.restaurantId, user.id],
|
|
);
|
|
await this.#refreshStat(like.restaurantId);
|
|
}
|
|
}
|
|
|
|
async getNearby({ userExternalId, lat, lng, radiusMeters, category, sort }) {
|
|
const user = await this.#getOrCreateUser(userExternalId);
|
|
const params = [user.id];
|
|
let whereClause = 'where r.is_active = 1';
|
|
|
|
if (category && CATEGORY_SET.has(category)) {
|
|
whereClause += ' and r.category = ?';
|
|
params.push(category);
|
|
}
|
|
|
|
const [rows] = await this.pool.query(
|
|
`select
|
|
r.id,
|
|
r.name,
|
|
r.category,
|
|
r.address,
|
|
r.phone,
|
|
r.lat,
|
|
r.lng,
|
|
coalesce(s.review_count, 0) as reviewCount,
|
|
coalesce(s.like_count, 0) as likeCount,
|
|
coalesce(s.average_rating, 0.00) as averageRating,
|
|
case when ul.user_id is null then 0 else 1 end as likedByMe
|
|
from restaurant r
|
|
left join restaurant_stat s on s.restaurant_id = r.id
|
|
left join restaurant_like ul on ul.restaurant_id = r.id and ul.user_id = ?
|
|
${whereClause}`,
|
|
params,
|
|
);
|
|
|
|
let items = rows
|
|
.map((row) => ({
|
|
id: row.id,
|
|
name: row.name,
|
|
category: row.category,
|
|
address: row.address,
|
|
phone: row.phone,
|
|
lat: Number(row.lat),
|
|
lng: Number(row.lng),
|
|
distanceMeters: Math.round(haversineMeters(lat, lng, Number(row.lat), Number(row.lng))),
|
|
averageRating: Number(row.averageRating),
|
|
reviewCount: Number(row.reviewCount),
|
|
likeCount: Number(row.likeCount),
|
|
likedByMe: Number(row.likedByMe) === 1,
|
|
}))
|
|
.filter((item) => item.distanceMeters <= radiusMeters);
|
|
|
|
return sortItems(items, sort);
|
|
}
|
|
|
|
async getRestaurantById(restaurantId, userExternalId) {
|
|
const user = await this.#getOrCreateUser(userExternalId);
|
|
const [rows] = await this.pool.query(
|
|
`select
|
|
r.id,
|
|
r.name,
|
|
r.category,
|
|
r.address,
|
|
r.phone,
|
|
r.lat,
|
|
r.lng,
|
|
coalesce(s.review_count, 0) as reviewCount,
|
|
coalesce(s.like_count, 0) as likeCount,
|
|
coalesce(s.average_rating, 0.00) as averageRating,
|
|
case when ul.user_id is null then 0 else 1 end as likedByMe
|
|
from restaurant r
|
|
left join restaurant_stat s on s.restaurant_id = r.id
|
|
left join restaurant_like ul on ul.restaurant_id = r.id and ul.user_id = ?
|
|
where r.id = ? and r.is_active = 1
|
|
limit 1`,
|
|
[user.id, restaurantId],
|
|
);
|
|
|
|
if (rows.length === 0) return null;
|
|
|
|
const row = rows[0];
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
category: row.category,
|
|
address: row.address,
|
|
phone: row.phone,
|
|
lat: Number(row.lat),
|
|
lng: Number(row.lng),
|
|
distanceMeters: 0,
|
|
averageRating: Number(row.averageRating),
|
|
reviewCount: Number(row.reviewCount),
|
|
likeCount: Number(row.likeCount),
|
|
likedByMe: Number(row.likedByMe) === 1,
|
|
};
|
|
}
|
|
|
|
async getReviews(restaurantId, userExternalId) {
|
|
const user = await this.#getOrCreateUser(userExternalId);
|
|
const [rows] = await this.pool.query(
|
|
`select
|
|
rr.id,
|
|
rr.restaurant_id as restaurantId,
|
|
rr.rating,
|
|
rr.content,
|
|
rr.created_at as createdAt,
|
|
rr.updated_at as updatedAt,
|
|
au.provider_user_id as userExternalId,
|
|
au.nick_name as nickName,
|
|
case when rr.user_id = ? then 1 else 0 end as mine
|
|
from restaurant_review rr
|
|
inner join app_user au on au.id = rr.user_id
|
|
where rr.restaurant_id = ? and rr.deleted_at is null
|
|
order by rr.created_at desc`,
|
|
[user.id, restaurantId],
|
|
);
|
|
|
|
return rows.map((row) => ({
|
|
id: row.id,
|
|
restaurantId: row.restaurantId,
|
|
userExternalId: row.userExternalId,
|
|
nickName: row.nickName,
|
|
rating: Number(row.rating),
|
|
content: row.content,
|
|
createdAt: toIso(row.createdAt),
|
|
updatedAt: toIso(row.updatedAt),
|
|
mine: Number(row.mine) === 1,
|
|
}));
|
|
}
|
|
|
|
async upsertReview(restaurantId, userExternalId, rating, content) {
|
|
const user = await this.#getOrCreateUser(userExternalId);
|
|
const [restaurantRows] = await this.pool.query(
|
|
`select id from restaurant where id = ? and is_active = 1 limit 1`,
|
|
[restaurantId],
|
|
);
|
|
if (restaurantRows.length === 0) return null;
|
|
|
|
const reviewId = uuidv4();
|
|
await this.pool.query(
|
|
`insert into restaurant_review
|
|
(id, restaurant_id, user_id, rating, content, created_at, updated_at, deleted_at)
|
|
values
|
|
(?, ?, ?, ?, ?, current_timestamp, current_timestamp, null)
|
|
on duplicate key update
|
|
rating = values(rating),
|
|
content = values(content),
|
|
deleted_at = null,
|
|
updated_at = current_timestamp`,
|
|
[reviewId, restaurantId, user.id, rating, content],
|
|
);
|
|
|
|
await this.#refreshStat(restaurantId);
|
|
|
|
const [rows] = await this.pool.query(
|
|
`select rr.id, rr.rating, rr.content, rr.created_at as createdAt, rr.updated_at as updatedAt
|
|
from restaurant_review rr
|
|
where rr.restaurant_id = ? and rr.user_id = ? and rr.deleted_at is null
|
|
limit 1`,
|
|
[restaurantId, user.id],
|
|
);
|
|
|
|
const row = rows[0];
|
|
return {
|
|
id: row.id,
|
|
restaurantId,
|
|
userExternalId,
|
|
nickName: user.nickName,
|
|
rating: Number(row.rating),
|
|
content: row.content,
|
|
createdAt: toIso(row.createdAt),
|
|
updatedAt: toIso(row.updatedAt),
|
|
mine: true,
|
|
};
|
|
}
|
|
|
|
async deleteReview(reviewId, userExternalId) {
|
|
const user = await this.#getOrCreateUser(userExternalId);
|
|
const [targetRows] = await this.pool.query(
|
|
`select restaurant_id as restaurantId, user_id as userId
|
|
from restaurant_review
|
|
where id = ? and deleted_at is null
|
|
limit 1`,
|
|
[reviewId],
|
|
);
|
|
|
|
if (targetRows.length === 0) {
|
|
return { deleted: false, reason: 'NOT_FOUND' };
|
|
}
|
|
|
|
const target = targetRows[0];
|
|
if (target.userId !== user.id) {
|
|
return { deleted: false, reason: 'FORBIDDEN' };
|
|
}
|
|
|
|
await this.pool.query(`delete from restaurant_review where id = ?`, [reviewId]);
|
|
await this.#refreshStat(target.restaurantId);
|
|
return { deleted: true };
|
|
}
|
|
|
|
async toggleLike(restaurantId, userExternalId) {
|
|
const user = await this.#getOrCreateUser(userExternalId);
|
|
|
|
const [restaurantRows] = await this.pool.query(
|
|
`select id from restaurant where id = ? and is_active = 1 limit 1`,
|
|
[restaurantId],
|
|
);
|
|
if (restaurantRows.length === 0) return null;
|
|
|
|
const [insertResult] = await this.pool.query(
|
|
`insert ignore into restaurant_like (restaurant_id, user_id) values (?, ?)`,
|
|
[restaurantId, user.id],
|
|
);
|
|
|
|
let liked = true;
|
|
if ((insertResult.affectedRows ?? 0) === 0) {
|
|
await this.pool.query(
|
|
`delete from restaurant_like where restaurant_id = ? and user_id = ?`,
|
|
[restaurantId, user.id],
|
|
);
|
|
liked = false;
|
|
}
|
|
|
|
await this.#refreshStat(restaurantId);
|
|
|
|
const [rows] = await this.pool.query(
|
|
`select count(*) as cnt from restaurant_like where restaurant_id = ?`,
|
|
[restaurantId],
|
|
);
|
|
|
|
return {
|
|
liked,
|
|
likeCount: Number(rows[0]?.cnt ?? 0),
|
|
};
|
|
}
|
|
|
|
async getMyReviews(userExternalId) {
|
|
const user = await this.#getOrCreateUser(userExternalId);
|
|
const [rows] = await this.pool.query(
|
|
`select
|
|
rr.id,
|
|
rr.restaurant_id as restaurantId,
|
|
rr.rating,
|
|
rr.content,
|
|
rr.created_at as createdAt,
|
|
rr.updated_at as updatedAt,
|
|
au.nick_name as nickName
|
|
from restaurant_review rr
|
|
inner join app_user au on au.id = rr.user_id
|
|
where rr.user_id = ? and rr.deleted_at is null
|
|
order by rr.created_at desc`,
|
|
[user.id],
|
|
);
|
|
|
|
return rows.map((row) => ({
|
|
id: row.id,
|
|
restaurantId: row.restaurantId,
|
|
userExternalId,
|
|
nickName: row.nickName,
|
|
rating: Number(row.rating),
|
|
content: row.content,
|
|
createdAt: toIso(row.createdAt),
|
|
updatedAt: toIso(row.updatedAt),
|
|
mine: true,
|
|
}));
|
|
}
|
|
|
|
async #getOrCreateUser(userExternalId) {
|
|
const [rows] = await this.pool.query(
|
|
`select id, nick_name as nickName
|
|
from app_user
|
|
where provider = 'toss' and provider_user_id = ?
|
|
limit 1`,
|
|
[userExternalId],
|
|
);
|
|
|
|
if (rows.length > 0) {
|
|
return { id: rows[0].id, nickName: rows[0].nickName };
|
|
}
|
|
|
|
const createdId = uuidv4();
|
|
const nickName = userExternalId === 'demo-user-1' ? '나' : '사용자';
|
|
await this.pool.query(
|
|
`insert into app_user (id, provider, provider_user_id, nick_name)
|
|
values (?, 'toss', ?, ?)`,
|
|
[createdId, userExternalId, nickName],
|
|
);
|
|
|
|
return { id: createdId, nickName };
|
|
}
|
|
|
|
async #refreshStat(restaurantId) {
|
|
await this.pool.query(`call refresh_restaurant_stat(?)`, [restaurantId]);
|
|
}
|
|
}
|
|
|
|
async function createStore() {
|
|
const mysqlEnabled = Boolean(process.env.MYSQL_HOST || process.env.MYSQL_URL);
|
|
if (!mysqlEnabled) {
|
|
return {
|
|
mode: 'memory',
|
|
store: new MemoryStore(),
|
|
};
|
|
}
|
|
|
|
const pool = process.env.MYSQL_URL
|
|
? mysql.createPool(process.env.MYSQL_URL)
|
|
: mysql.createPool({
|
|
host: process.env.MYSQL_HOST,
|
|
port: Number(process.env.MYSQL_PORT || 3306),
|
|
user: process.env.MYSQL_USER,
|
|
password: process.env.MYSQL_PASSWORD,
|
|
database: process.env.MYSQL_DATABASE,
|
|
waitForConnections: true,
|
|
connectionLimit: 10,
|
|
});
|
|
|
|
const store = new MySQLStore(pool);
|
|
await store.bootstrap();
|
|
|
|
return {
|
|
mode: 'mysql',
|
|
store,
|
|
};
|
|
}
|
|
|
|
function sortItems(items, sort) {
|
|
const copied = [...items];
|
|
|
|
if (sort === 'rating') {
|
|
return copied.sort((a, b) => {
|
|
if (b.averageRating === a.averageRating) {
|
|
return b.reviewCount - a.reviewCount;
|
|
}
|
|
return b.averageRating - a.averageRating;
|
|
});
|
|
}
|
|
|
|
if (sort === 'likes') {
|
|
return copied.sort((a, b) => {
|
|
if (b.likeCount === a.likeCount) {
|
|
return a.distanceMeters - b.distanceMeters;
|
|
}
|
|
return b.likeCount - a.likeCount;
|
|
});
|
|
}
|
|
|
|
return copied.sort((a, b) => a.distanceMeters - b.distanceMeters);
|
|
}
|
|
|
|
function roundTo1(value) {
|
|
return Math.round(value * 10) / 10;
|
|
}
|
|
|
|
function haversineMeters(lat1, lng1, lat2, lng2) {
|
|
const toRad = (value) => (value * Math.PI) / 180;
|
|
const earthRadius = 6371000;
|
|
|
|
const dLat = toRad(lat2 - lat1);
|
|
const dLng = toRad(lng2 - lng1);
|
|
|
|
const a =
|
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
|
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return earthRadius * c;
|
|
}
|
|
|
|
function toIso(dateLike) {
|
|
return new Date(dateLike).toISOString();
|
|
}
|
|
|
|
module.exports = {
|
|
createStore,
|
|
};
|