summaryrefslogtreecommitdiff
path: root/lib/items-ship/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/items-ship/service.ts')
-rw-r--r--lib/items-ship/service.ts416
1 files changed, 416 insertions, 0 deletions
diff --git a/lib/items-ship/service.ts b/lib/items-ship/service.ts
new file mode 100644
index 00000000..37b623c1
--- /dev/null
+++ b/lib/items-ship/service.ts
@@ -0,0 +1,416 @@
+// src/lib/items-ship/service.ts
+"use server"; // Next.js 서버 액션에서 직접 import하려면 (선택)
+
+import { revalidateTag, unstable_noStore } from "next/cache";
+import db from "@/db/db";
+
+import { filterColumns } from "@/lib/filter-columns";
+import { unstable_cache } from "@/lib/unstable-cache";
+import { getErrorMessage } from "@/lib/handle-error";
+
+import { asc, desc, ilike, and, or, eq, count, inArray, sql } from "drizzle-orm";
+import { GetItemsSchema, UpdateItemSchema, ShipbuildingItemCreateData, TypedItemCreateData } from "./validations";
+import { Item, items, itemShipbuilding } from "@/db/schema/items";
+import { deleteItemById, deleteItemsByIds, findAllItems, insertItem, updateItem } from "./repository";
+
+/* -----------------------------------------------------
+ 1) 조회 관련
+----------------------------------------------------- */
+
+/**
+ * 복잡한 조건으로 Item 목록을 조회 (+ pagination) 하고,
+ * 총 개수에 따라 pageCount를 계산해서 리턴.
+ * Next.js의 unstable_cache를 사용해 일정 시간 캐시.
+ */
+export async function getShipbuildingItems(input: GetItemsSchema) {
+ return unstable_cache(
+ async () => {
+ try {
+ const offset = (input.page - 1) * input.perPage;
+
+ // advancedTable 모드면 filterColumns()로 where 절 구성
+ const advancedWhere = filterColumns({
+ table: items,
+ filters: input.filters,
+ joinOperator: input.joinOperator,
+ });
+
+ let globalWhere;
+ if (input.search) {
+ const s = `%${input.search}%`;
+ globalWhere = or(
+ ilike(items.itemCode, s),
+ ilike(items.itemName, s),
+ ilike(items.description, s)
+ );
+ }
+
+ const finalWhere = and(
+ advancedWhere,
+ globalWhere
+ );
+
+ const where = finalWhere;
+
+ const orderBy =
+ input.sort.length > 0
+ ? input.sort.map((item) =>
+ item.desc ? desc(items[item.id]) : asc(items[item.id])
+ )
+ : [asc(items.createdAt)];
+
+ // 조선 아이템 테이블과 기본 아이템 테이블 조인하여 조회
+ const result = await db.select({
+ id: itemShipbuilding.id,
+ itemId: itemShipbuilding.itemId,
+ workType: itemShipbuilding.workType,
+ shipTypes: itemShipbuilding.shipTypes,
+ itemCode: items.itemCode,
+ itemName: items.itemName,
+ description: items.description,
+ createdAt: items.createdAt,
+ updatedAt: items.updatedAt,
+ })
+ .from(itemShipbuilding)
+ .innerJoin(items, eq(itemShipbuilding.itemId, items.id))
+ .where(where)
+ .orderBy(...orderBy)
+ .offset(offset)
+ .limit(input.perPage);
+
+ // 전체 데이터 개수 조회
+ const [{ count: total }] = await db.select({
+ count: count()
+ })
+ .from(itemShipbuilding)
+ .innerJoin(items, eq(itemShipbuilding.itemId, items.id))
+ .where(where);
+
+ const pageCount = Math.ceil(Number(total) / input.perPage);
+
+ return { data: result, pageCount };
+ } catch (err) {
+ console.error("Error fetching shipbuilding items:", err);
+ return { data: [], pageCount: 0 };
+ }
+ },
+ [JSON.stringify(input)],
+ {
+ revalidate: 3600,
+ tags: ["items"],
+ }
+ )();
+}
+
+/* -----------------------------------------------------
+ 2) 생성(Create)
+----------------------------------------------------- */
+
+/**
+ * Item 생성 - 아이템 타입에 따라 해당 테이블에 데이터 삽입
+ */
+export async function createShipbuildingItem(input: TypedItemCreateData) {
+ unstable_noStore()
+
+ try {
+ if (!input.itemCode || !input.itemName) {
+ return {
+ success: false,
+ message: "아이템 코드와 아이템 명은 필수입니다",
+ data: null,
+ error: "필수 필드 누락"
+ }
+ }
+
+ let result: unknown[] = []
+
+ result = await db.transaction(async (tx) => {
+ const existingItem = await tx.query.items.findFirst({
+ where: eq(items.itemCode, input.itemCode),
+ })
+
+ let itemId: number
+ let itemResult
+
+ if (existingItem) {
+ itemResult = await updateItem(tx, existingItem.id, {
+ itemName: input.itemName,
+ description: input.description,
+ })
+ itemId = existingItem.id
+ } else {
+ itemResult = await insertItem(tx, {
+ itemCode: input.itemCode,
+ itemName: input.itemName,
+ description: input.description,
+ })
+ itemId = itemResult[0].id
+ }
+
+ const shipData = input as ShipbuildingItemCreateData;
+ const typeResult = await tx.insert(itemShipbuilding).values({
+ itemId: itemId,
+ workType: shipData.workType ? (shipData.workType as '기장' | '전장' | '선실' | '배관' | '철의') : '기장',
+ shipTypes: shipData.shipTypes || ''
+ }).returning();
+
+ return [...itemResult, ...typeResult]
+ })
+
+ revalidateTag("items")
+
+ return {
+ success: true,
+ data: result[0] || null,
+ error: null
+ }
+ } catch (err) {
+ console.error("아이템 생성/업데이트 오류:", err)
+
+ if (err instanceof Error && err.message.includes("unique constraint")) {
+ return {
+ success: false,
+ message: "이미 존재하는 아이템 코드입니다",
+ data: null,
+ error: "중복 키 오류"
+ }
+ }
+
+ return {
+ success: false,
+ message: getErrorMessage(err),
+ data: null,
+ error: getErrorMessage(err)
+ }
+ }
+}
+
+/**
+ * Excel import를 위한 조선 아이템 생성 함수
+ * 하나의 아이템 코드에 대해 여러 선종을 처리 (1:N 관계)
+ */
+export async function createShipbuildingImportItem(input: {
+ itemCode: string;
+ itemName: string;
+ workType: '기장' | '전장' | '선실' | '배관' | '철의';
+ description?: string | null;
+ shipTypes: Record<string, boolean>;
+}) {
+ unstable_noStore();
+
+ try {
+
+ if (!input.itemCode || !input.itemName) {
+ return {
+ success: false,
+ message: "아이템 코드와 아이템 명은 필수입니다",
+ data: null,
+ error: "필수 필드 누락"
+ }
+ }
+ let results: any[] = []
+ results = await db.transaction(async (tx) => {
+ // 1. itemCode 정규화해서 직접 쿼리
+ const existRows = await tx.select().from(items)
+ .where(eq(items.itemCode, input.itemCode));
+ const existingItem = existRows[0];
+
+ console.log('DB에서 직접 조회한 기존 아이템:', existingItem);
+
+ let itemId: number;
+
+ if (existingItem) {
+ // 이미 있으면 업데이트
+ await updateItem(tx, existingItem.id, {
+ itemName: input.itemName,
+ description: input.description,
+ });
+ itemId = existingItem.id;
+ console.log('기존 아이템 업데이트, id:', itemId);
+ } else {
+ // 없으면 새로 생성
+ // 현재 가장 큰 ID 값 가져오기
+ const maxIdResult = await tx.select({ maxId: sql`MAX(id)` }).from(items);
+ const maxId = maxIdResult[0]?.maxId || 0;
+ const newId = Number(maxId) + 1;
+ console.log('새 아이템 생성을 위한 ID 계산:', { maxId, newId });
+
+ // 새 ID로 아이템 생성
+ const insertResult = await tx.insert(items).values({
+ id: newId,
+ itemCode: input.itemCode,
+ itemName: input.itemName,
+ description: input.description,
+ }).returning();
+
+ itemId = insertResult[0].id;
+ console.log('새 아이템 생성 완료, id:', itemId);
+ }
+
+ const createdItems = [];
+ for (const shipType of Object.keys(input.shipTypes)) {
+ // 그대로 선종명 string으로 저장
+ const existShip = await tx.select().from(itemShipbuilding)
+ .where(
+ and(
+ eq(itemShipbuilding.itemId, itemId),
+ eq(itemShipbuilding.shipTypes, shipType)
+ )
+ );
+ if (!existShip[0]) {
+ const shipbuildingResult = await tx.insert(itemShipbuilding).values({
+ itemId: itemId,
+ workType: input.workType,
+ shipTypes: shipType
+ }).returning();
+ createdItems.push({
+ ...shipbuildingResult[0]
+ });
+ console.log('조선아이템 생성:', shipType, shipbuildingResult[0]);
+ } else {
+ console.log('이미 존재하는 조선아이템:', shipType);
+ }
+ }
+ return createdItems;
+ });
+
+ revalidateTag("items");
+
+ return {
+ success: true,
+ data: results,
+ error: null
+ }
+ } catch (err) {
+ // DB에 실제로 존재하는 itemCode 목록도 함께 출력
+ const allCodes = await db.select({ code: items.itemCode }).from(items);
+ console.error("아이템 import 오류:", err);
+ console.error("DB에 존재하는 모든 itemCode:", allCodes.map(x => x.code));
+ return {
+ success: false,
+ message: getErrorMessage(err),
+ data: null,
+ error: getErrorMessage(err)
+ }
+ }
+}
+
+/* -----------------------------------------------------
+ 3) 업데이트
+----------------------------------------------------- */
+
+// 업데이트 타입 정의 인터페이스
+interface UpdateShipbuildingItemInput extends UpdateItemSchema {
+ id: number;
+ workType?: string;
+ shipTypes?: string;
+ itemCode?: string;
+ itemName?: string;
+ description?: string;
+}
+
+/** 단건 업데이트 */
+export async function modifyShipbuildingItem(input: UpdateShipbuildingItemInput) {
+ unstable_noStore();
+ try {
+ await db.transaction(async (tx) => {
+ // 기본 아이템 테이블 업데이트
+ const [item] = await updateItem(tx, input.id, {
+ itemCode: input.itemCode,
+ itemName: input.itemName,
+ description: input.description,
+ });
+
+ // 조선 아이템 테이블 업데이트
+ if (input.workType || input.shipTypes) {
+ await tx.update(itemShipbuilding)
+ .set({
+ workType: input.workType as '기장' | '전장' | '선실' | '배관' | '철의',
+ shipTypes: input.shipTypes
+ })
+ .where(eq(itemShipbuilding.itemId, item.id));
+ }
+
+ return item;
+ });
+
+ revalidateTag("items");
+ return { data: null, error: null };
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
+}
+
+/* -----------------------------------------------------
+ 4) 삭제
+----------------------------------------------------- */
+
+// 삭제 타입 정의 인터페이스
+interface DeleteItemInput {
+ id: number;
+}
+
+interface DeleteItemsInput {
+ ids: number[];
+}
+
+/** 단건 삭제 */
+export async function removeShipbuildingItem(input: DeleteItemInput) {
+ unstable_noStore();
+ try {
+ await db.transaction(async (tx) => {
+ const item = await tx.query.items.findFirst({
+ where: eq(items.id, input.id),
+ });
+
+ if (!item) {
+ throw new Error("아이템을 찾을 수 없습니다.");
+ }
+
+ // 조선 아이템 테이블에서 먼저 삭제
+ await tx.delete(itemShipbuilding)
+ .where(eq(itemShipbuilding.itemId, input.id));
+
+ // 기본 아이템 테이블에서 삭제
+ await deleteItemById(tx, input.id);
+ });
+
+ revalidateTag("items");
+
+ return { data: null, error: null };
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
+}
+
+/** 복수 삭제 */
+export async function removeShipbuildingItems(input: DeleteItemsInput) {
+ unstable_noStore();
+ try {
+ await db.transaction(async (tx) => {
+ if (input.ids.length > 0) {
+ // 조선 아이템 테이블에서 먼저 삭제
+ await tx.delete(itemShipbuilding)
+ .where(inArray(itemShipbuilding.itemId, input.ids));
+
+ // 기본 아이템 테이블에서 삭제
+ await deleteItemsByIds(tx, input.ids);
+ }
+ });
+
+ revalidateTag("items");
+
+ return { data: null, error: null, success: true, message: "아이템이 성공적으로 삭제되었습니다." };
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err), success: false, message: "아이템 삭제 중 오류가 발생했습니다." };
+ }
+}
+
+export async function getAllShipbuildingItems(): Promise<Item[]> {
+ try {
+ return await findAllItems();
+ } catch (error) {
+ console.error("Failed to get items:", error);
+ throw new Error("Failed to get items");
+ }
+}