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
29 changes: 23 additions & 6 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,37 @@ dev-down:
bash "{{ repo }}/scripts/local-deps-down.sh"

# DB 초기화 + 마이그레이션 + seed — 깨끗한 상태로 리셋 (Supabase CLI 필요)
# PRD_DB_URL 이 env 또는 .env.local 에 있으면 PRD 데이터로 시딩, 없으면 로컬 seed.sql 사용
dev-reset:
#!/usr/bin/env bash
set -euo pipefail
if ! command -v supabase >/dev/null 2>&1; then
echo "❌ supabase CLI not found. Install: brew install supabase/tap/supabase"
exit 1
fi
echo "⚠️ Supabase 로컬 DB 를 리셋합니다 (볼륨 유지, schema 재적용)..."
( cd "{{ repo }}" && supabase db reset ) || true
echo "⏳ Waiting 3s for postgres..."
sleep 3

# PRD_DB_URL 자동 감지: env → packages/web/.env.local
if [ -z "${PRD_DB_URL:-}" ]; then
ENV_FILE="{{ repo }}/packages/web/.env.local"
if [ -f "$ENV_FILE" ]; then
PRD_DB_URL=$(grep '^PRD_DB_URL=' "$ENV_FILE" | cut -d'=' -f2- || true)
export PRD_DB_URL
fi
fi

if [ -n "${PRD_DB_URL:-}" ]; then
echo "🔄 PRD 데이터로 리셋합니다 (seed-from-prod --yes)..."
bash "{{ repo }}/scripts/seed-from-prod.sh" --yes
else
echo "⚠️ PRD_DB_URL 없음 — 로컬 seed.sql 로 리셋합니다..."
( cd "{{ repo }}" && supabase db reset ) || true
echo "⏳ Waiting 3s for postgres..."
sleep 3
just seed || echo "⚠️ seed 실패 (Auth 유저 FK 등) — Studio 에서 유저 생성 후 재시도"
fi

# supabase db reset 은 컨테이너를 재시작하므로 decoded-backend 네트워크 재연결 필요
bash "{{ repo }}/scripts/local-deps-connect.sh"
just seed || echo "⚠️ seed 실패 (Auth 유저 FK 등) — Studio 에서 유저 생성 후 재시도"
bash "{{ repo }}/scripts/local-deps-connect.sh" 2>/dev/null || echo "ℹ️ 네트워크 재연결 건너뜀 (just local-deps 미실행 상태)"
echo "✅ DB reset 완료. Start apps with: just dev"

# seed.sql 적용 — postgres 가 기동 중이어야 함 (Supabase CLI 기본 DB)
Expand Down
1 change: 1 addition & 0 deletions packages/api-server/deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ignore = [
# 현재 crates.io 최신 호환 버전 범위에서도 lockfile이 이동하지 않아, SDK transport 업그레이드 시 재검토한다.
{ id = "RUSTSEC-2026-0098", reason = "AWS SDK TLS 체인이 rustls-webpki 0.101.x에 고정; SDK transport 업그레이드 시 재검토" },
{ id = "RUSTSEC-2026-0099", reason = "AWS SDK TLS 체인이 rustls-webpki 0.101.x에 고정; SDK transport 업그레이드 시 재검토" },
{ id = "RUSTSEC-2026-0104", reason = "AWS SDK TLS 체인이 rustls-webpki 0.101.x에 고정; SDK transport 업그레이드 시 재검토" },
]

[licenses]
Expand Down
5 changes: 4 additions & 1 deletion packages/api-server/src/bin/dump_openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ fn main() {
let mut f = fs::File::create(&out).expect("create openapi.json");
f.write_all(json.as_bytes()).expect("write openapi.json");
f.write_all(b"\n").expect("newline");
println!("Wrote {} ({} bytes)", out, json.len());
#[allow(clippy::disallowed_macros)]
{
println!("Wrote {} ({} bytes)", out, json.len());
}
}
7 changes: 5 additions & 2 deletions packages/api-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,11 @@ pub struct EmbeddingConfig {
impl AppConfig {
pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
// 로컬 개발: `.env.dev` 우선, 이어서 `.env` (이미 설정된 변수는 dotenvy가 덮어쓰지 않음)
let _ = dotenvy::from_filename(".env.dev");
let _ = dotenvy::dotenv();
#[cfg(not(test))]
{
let _ = dotenvy::from_filename(".env.dev");
let _ = dotenvy::dotenv();
}

let app_env = AppEnv::from_env();

Expand Down
2 changes: 1 addition & 1 deletion packages/api-server/src/domains/admin/gemini_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ pub async fn get_top_raw_posts(
GROUP BY raw_post_id
ORDER BY spend_usd DESC
LIMIT $2",
vec![(days as i32).into(), (limit as i64).into()],
vec![(days as i32).into(), limit.into()],
);
let rows = db.query_all(stmt).await.map_err(AppError::DatabaseError)?;
let mut out = Vec::with_capacity(rows.len());
Expand Down
1 change: 1 addition & 0 deletions packages/api-server/src/domains/content_studio/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ pub async fn persist_packet(
Ok(())
}

#[allow(clippy::disallowed_methods)]
pub fn generate_channel_variants(packet: &ContentPacket) -> Vec<ContentVariant> {
let tags = vec![
"decoded".to_string(),
Expand Down
1 change: 1 addition & 0 deletions packages/api-server/src/domains/posts/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2270,6 +2270,7 @@ pub async fn list_tries_by_spot(
///
/// `<C: ConnectionTrait>` 로 트랜잭션 안에서 호출 가능 — verify 가 spots/solutions
/// 까지 한 트랜잭션으로 묶을 수 있도록 변경 (#350).
#[allow(clippy::too_many_arguments)]
pub async fn create_post_from_raw<C: sea_orm::ConnectionTrait>(
db: &C,
post_id: Uuid,
Expand Down
2 changes: 2 additions & 0 deletions packages/api-server/src/domains/raw_posts/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ pub async fn list_events(
(status = 502, description = "ai-server gRPC 통신 실패")
)
)]
#[allow(clippy::disallowed_methods)]
pub async fn trigger_source(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Expand Down Expand Up @@ -495,6 +496,7 @@ pub struct ReparseRawPostDto {
(status = 502, description = "ai-server gRPC 통신 실패")
)
)]
#[allow(clippy::disallowed_methods)]
pub async fn reparse_item(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Expand Down
1 change: 1 addition & 0 deletions packages/api-server/src/domains/raw_posts/relocate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ mod tests {
/// 테스트용 in-memory StorageClient — download 가 받은 bytes 를 upload 가
/// 받았는지 검증할 수 있도록 호출 기록 보존.
struct InMemoryStorage {
#[allow(clippy::type_complexity)]
objects: Mutex<HashMap<String, (Vec<u8>, Option<String>)>>,
upload_log: Mutex<Vec<(String, Vec<u8>, String)>>,
public_url: String,
Expand Down
2 changes: 1 addition & 1 deletion packages/api-server/src/domains/raw_posts/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ fn parse_whatsonthestar_source(raw: &str) -> Result<(String, String), String> {
&& trimmed.contains('-')
&& trimmed
.split('-')
.last()
.next_back()
.is_some_and(|tail| !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()));
if valid {
return Ok(("outfit".to_string(), trimmed.to_string()));
Expand Down
3 changes: 2 additions & 1 deletion packages/api-server/src/domains/solutions/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ use super::dto::{
/// 판정한 외부 상품 URL, 또는 admin 이 verify 화면에서 입력/수정한 URL.
/// None 이면 NULL 로 INSERT (안전 default).
/// - `<C: ConnectionTrait>` 로 트랜잭션 안에서 호출 가능
/// 반환값은 row id (#350).
/// 반환값은 row id (#350).
#[allow(clippy::too_many_arguments)]
pub async fn create_solution_for_verify<C: ConnectionTrait>(
db: &C,
solution_id: Uuid,
Expand Down
5 changes: 0 additions & 5 deletions packages/api-server/src/domains/solutions/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ mod mock_db_tests {
has_url: None,
sort: "recent".to_string(),
pagination: Pagination::new(1, 20),
has_url: None,
};
let result = service::admin_list_solutions(&db, query).await;
assert!(result.is_ok(), "unexpected err: {:?}", result.err());
Expand Down Expand Up @@ -262,7 +261,6 @@ mod mock_db_tests {
has_url: None,
sort: "popular".to_string(),
pagination: Pagination::new(1, 20),
has_url: None,
};
assert!(service::admin_list_solutions(&db, query).await.is_ok());
}
Expand All @@ -289,7 +287,6 @@ mod mock_db_tests {
has_url: None,
sort: "verified".to_string(),
pagination: Pagination::new(1, 20),
has_url: None,
};
assert!(service::admin_list_solutions(&db, query).await.is_ok());
}
Expand All @@ -316,7 +313,6 @@ mod mock_db_tests {
has_url: None,
sort: "adopted".to_string(),
pagination: Pagination::new(1, 20),
has_url: None,
};
assert!(service::admin_list_solutions(&db, query).await.is_ok());
}
Expand All @@ -343,7 +339,6 @@ mod mock_db_tests {
has_url: None,
sort: "unknown_sort_key".to_string(),
pagination: Pagination::new(1, 20),
has_url: None,
};
assert!(service::admin_list_solutions(&db, query).await.is_ok());
}
Expand Down
4 changes: 2 additions & 2 deletions packages/api-server/src/domains/spots/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ pub async fn get_category_from_spot(
/// - subcategory 존재 검증을 호출자가 책임짐 (lookup 로직 분리).
/// - DTO 대신 단순 위치값을 받음.
/// - `<C: ConnectionTrait>` 로 트랜잭션 안에서 호출 가능.
/// 반환값은 `SpotResponse` 가 아니라 raw `SpotModel` — 호출 측이 추가 작업
/// (solution 연결) 만 하므로 가벼운 모델로 족함 (#350).
/// 반환값은 `SpotResponse` 가 아니라 raw `SpotModel` — 호출 측이 추가 작업
/// (solution 연결) 만 하므로 가벼운 모델로 족함 (#350).
pub async fn create_spot_for_verify<C: ConnectionTrait>(
db: &C,
post_id: Uuid,
Expand Down
1 change: 1 addition & 0 deletions packages/web/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ coverage

# Next.js
.next
.next.bak
out
build
dist
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,11 @@ describe("AssetPanel", () => {
/>
);

fireEvent.click(
view.getByRole("button", { name: "Generate Asset Plan" })
);
fireEvent.click(view.getByRole("button", { name: "Generate Asset Plan" }));

await waitFor(() => {
expect(view.getByText("Editorial airport denim still")).toBeTruthy();
});
expect(
view.getByLabelText("Copy prompt for instagram_feed")
).toBeTruthy();
expect(view.getByLabelText("Copy prompt for instagram_feed")).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,15 @@ describe("ShortFormPanel", () => {
/>
);

fireEvent.click(
view.getByRole("button", { name: "Generate Short Form" })
);
fireEvent.click(view.getByRole("button", { name: "Generate Short Form" }));

await waitFor(() => {
expect(
view.getByText("Why this airport look reads premium")
).toBeTruthy();
});
expect(
view.getByText(
"The silhouette does the work before the brand does."
)
view.getByText("The silhouette does the work before the brand does.")
).toBeTruthy();
expect(view.getByTestId("scene-1")).toBeTruthy();
});
Expand Down
4 changes: 2 additions & 2 deletions packages/web/app/admin/content-studio/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async function postJson<T>(url: string, body: unknown): Promise<T> {
? data.message
: data && typeof data.error === "string"
? data.error
: `HTTP ${response.status}`;
: `HTTP ${response.status}`;
throw new Error(message);
}
return data as T;
Expand All @@ -68,7 +68,7 @@ async function getJson<T>(url: string): Promise<T> {
? data.message
: data && typeof data.error === "string"
? data.error
: `HTTP ${response.status}`;
: `HTTP ${response.status}`;
throw new Error(message);
}
return data as T;
Expand Down
9 changes: 4 additions & 5 deletions packages/web/app/admin/editorial/magazine/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ export default function RecommendationDetailPage({
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
<AdminStatusBadge status={item.status} />
{item.score != null && (
<span className="tabular-nums">score {item.score.toFixed(2)}</span>
<span className="tabular-nums">
score {item.score.toFixed(2)}
</span>
)}
<span>·</span>
<span>{new Date(item.created_at).toLocaleString("ko-KR")}</span>
Expand Down Expand Up @@ -230,10 +232,7 @@ export default function RecommendationDetailPage({
</h2>
<ul className="space-y-1">
{item.source_solution_ids.map((sid) => (
<li
key={sid}
className="text-xs font-mono text-muted-foreground"
>
<li key={sid} className="text-xs font-mono text-muted-foreground">
{sid}
</li>
))}
Expand Down
4 changes: 1 addition & 3 deletions packages/web/app/admin/editorial/magazine/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ function MagazineAnglesContent() {
onReject={handleRejectOpen}
onDelete={(item) => {
if (
confirm(
`정말 삭제? 복구 불가\n\nangle: ${item.angle_title}`
)
confirm(`정말 삭제? 복구 불가\n\nangle: ${item.angle_title}`)
) {
setPendingId(item.id);
del.mutate(item.id, {
Expand Down
14 changes: 7 additions & 7 deletions packages/web/app/admin/raw-post-sources/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -741,13 +741,13 @@ function BulkPasteModal({
],
}
: {
description:
"Pinterest 핀 URL 또는 숫자 pin ID 를 줄 단위로 붙여넣기. source_type 은 자동 추론됩니다.",
placeholders: [
"https://www.pinterest.com/pin/925067579727148171/",
"925067579727148172",
],
};
description:
"Pinterest 핀 URL 또는 숫자 pin ID 를 줄 단위로 붙여넣기. source_type 은 자동 추론됩니다.",
placeholders: [
"https://www.pinterest.com/pin/925067579727148171/",
"925067579727148172",
],
};

return (
<div
Expand Down
10 changes: 7 additions & 3 deletions packages/web/app/admin/solutions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ function SolutionRow({ row }: { row: SolutionListItem }) {
{research && (
<div className="md:col-span-2 mt-1 rounded-md border border-border bg-muted/30 p-2 text-[11px] space-y-1">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">AI suggestion:</span>
<span className="font-medium text-foreground">
AI suggestion:
</span>
{research.confidence && (
<span
className={`inline-flex items-center px-1.5 py-0.5 rounded border text-[10px] ${
Expand Down Expand Up @@ -207,7 +209,8 @@ function SolutionRow({ row }: { row: SolutionListItem }) {
)}
<div className="md:col-span-2 flex items-center justify-between pt-1">
<span className="text-[10px] text-muted-foreground tabular-nums">
id: {row.id.slice(0, 8)} · {new Date(row.created_at).toLocaleDateString()} ·{" "}
id: {row.id.slice(0, 8)} ·{" "}
{new Date(row.created_at).toLocaleDateString()} ·{" "}
{row.is_verified ? "verified" : "unverified"}
{row.is_adopted ? " · adopted" : ""}
</span>
Expand All @@ -218,7 +221,8 @@ function SolutionRow({ row }: { row: SolutionListItem }) {
edit.mutate({
id: row.id,
input: {
original_url: url !== (row.original_url ?? "") ? url : undefined,
original_url:
url !== (row.original_url ?? "") ? url : undefined,
brand: brand !== getBrand(row.metadata) ? brand : undefined,
title: title !== row.title ? title : undefined,
},
Expand Down
6 changes: 5 additions & 1 deletion packages/web/app/api/admin/entities/artists/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { checkIsAdmin } from "@/lib/supabase/admin";
Expand Down Expand Up @@ -88,7 +89,10 @@ export async function DELETE(
.eq("id", id)
.single();

const { error } = await (supabase as any).from("artists").delete().eq("id", id);
const { error } = await (supabase as any)
.from("artists")
.delete()
.eq("id", id);

if (error)
return NextResponse.json({ error: error.message }, { status: 500 });
Expand Down
1 change: 1 addition & 0 deletions packages/web/app/api/admin/entities/artists/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { checkIsAdmin } from "@/lib/supabase/admin";
Expand Down
6 changes: 5 additions & 1 deletion packages/web/app/api/admin/entities/brands/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { checkIsAdmin } from "@/lib/supabase/admin";
Expand Down Expand Up @@ -88,7 +89,10 @@ export async function DELETE(
.eq("id", id)
.single();

const { error } = await (supabase as any).from("brands").delete().eq("id", id);
const { error } = await (supabase as any)
.from("brands")
.delete()
.eq("id", id);

if (error)
return NextResponse.json({ error: error.message }, { status: 500 });
Expand Down
1 change: 1 addition & 0 deletions packages/web/app/api/admin/entities/brands/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { checkIsAdmin } from "@/lib/supabase/admin";
Expand Down
1 change: 1 addition & 0 deletions packages/web/app/api/admin/entities/group-members/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { checkIsAdmin } from "@/lib/supabase/admin";
Expand Down
Loading
Loading