메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
del은 ID λ°°μ—΄λ‘œ μ—¬λŸ¬ λ ˆμ½”λ“œλ₯Ό ν•œ λ²ˆμ— μ‚­μ œν•˜λŠ” λ©”μ„œλ“œμž…λ‹ˆλ‹€. νŠΈλžœμž­μ…˜ λ‚΄μ—μ„œ μ•ˆμ „ν•˜κ²Œ μ‹€ν–‰λ˜λ©°, 기본적으둜 κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€.
del은 BaseModelClass에 μ •μ˜λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€. Entityλ₯Ό μƒμ„±ν•˜λ©΄ Syncerκ°€ 각 Model ν΄λž˜μŠ€μ— μžλ™μœΌλ‘œ μƒμ„±ν•˜λŠ” ν‘œμ€€ νŒ¨ν„΄μž…λ‹ˆλ‹€.

νƒ€μž… μ‹œκ·Έλ‹ˆμ²˜

async del(ids: number[]): Promise<number>

μžλ™ 생성 μ½”λ“œ

SonamuλŠ” Entityλ₯Ό 기반으둜 λ‹€μŒ μ½”λ“œλ₯Ό μžλ™μœΌλ‘œ μƒμ„±ν•©λ‹ˆλ‹€:
// src/application/user/user.model.ts (μžλ™ 생성)
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"], guards: ["admin"] })
  async del(ids: number[]): Promise<number> {
    const wdb = this.getPuri("w");

    // νŠΈλžœμž­μ…˜ λ‚΄μ—μ„œ μ‚­μ œ
    await wdb.transaction(async (trx) => {
      return trx.table("users").whereIn("users.id", ids).delete();
    });

    return ids.length;
  }
}
λ‚΄λΆ€ λ™μž‘:
  1. getPuri(β€œw”): BaseModelClass λ©”μ„œλ“œλ‘œ μ“°κΈ°μš© Puri νšλ“
  2. transaction(): νŠΈλžœμž­μ…˜ μ‹œμž‘
  3. table().whereIn().delete(): Knex λ©”μ„œλ“œλ‘œ μ‚­μ œ μ‹€ν–‰
  4. ids.length λ°˜ν™˜: μ‚­μ œ μš”μ²­ν•œ ID 개수 λ°˜ν™˜

λ§€κ°œλ³€μˆ˜

ids

μ‚­μ œν•  λ ˆμ½”λ“œμ˜ ID λ°°μ—΄μž…λ‹ˆλ‹€. νƒ€μž…: number[]
// 단일 λ ˆμ½”λ“œ μ‚­μ œ
await UserModel.del([123]);

// μ—¬λŸ¬ λ ˆμ½”λ“œ μ‚­μ œ
await UserModel.del([1, 2, 3, 4, 5]);

// 빈 λ°°μ—΄ (아무것도 μ‚­μ œν•˜μ§€ μ•ŠμŒ)
await UserModel.del([]);

λ°˜ν™˜κ°’

νƒ€μž…: Promise<number> μ‚­μ œλœ λ ˆμ½”λ“œμ˜ 개수λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
// 3개 μ‚­μ œ
const count = await UserModel.del([1, 2, 3]);
console.log(`${count} users deleted`);  // "3 users deleted"

// μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ID 포함
const count = await UserModel.del([1, 999, 1000]);
console.log(`${count} users deleted`);  // "1 users deleted" (1번만 쑴재)
μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” IDλŠ” λ¬΄μ‹œλ˜λ©° μ—λŸ¬λ₯Ό λ°œμƒμ‹œν‚€μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

κΈ°λ³Έ μ‚¬μš©λ²•

단일 λ ˆμ½”λ“œ μ‚­μ œ

import { UserModel } from "./user/user.model";

class UserService {
  async deleteUser(userId: number) {
    const count = await UserModel.del([userId]);
    
    if (count === 0) {
      throw new Error("User not found");
    }
    
    return { success: true };
  }
}

μ—¬λŸ¬ λ ˆμ½”λ“œ μ‚­μ œ

async deleteUsers(userIds: number[]) {
  const count = await UserModel.del(userIds);
  
  return {
    requested: userIds.length,
    deleted: count
  };
}

쑰건뢀 μ‚­μ œ

async deleteInactiveUsers() {
  // λΉ„ν™œμ„± μ‚¬μš©μž 쑰회
  const { rows } = await UserModel.findMany("A", {
    status: "inactive",
    num: 0
  });
  
  // ID μΆ”μΆœν•˜μ—¬ μ‚­μ œ
  const ids = rows.map(user => user.id);
  const count = await UserModel.del(ids);
  
  return { deleted: count };
}

νŠΈλžœμž­μ…˜

del은 μžλ™μœΌλ‘œ νŠΈλžœμž­μ…˜ λ‚΄μ—μ„œ μ‹€ν–‰λ©λ‹ˆλ‹€.
async del(ids: number[]): Promise<number> {
  const wdb = this.getPuri("w");

  // νŠΈλžœμž­μ…˜
  await wdb.transaction(async (trx) => {
    return trx.table("users")
      .whereIn("users.id", ids)
      .delete();
  });

  return ids.length;
}

@transactionalκ³Ό ν•¨κ»˜

import { api, transactional } from "sonamu";

class UserFrame {
  @api({ httpMethod: "POST" })
  @transactional()
  async deleteUserAndRelated(userId: number) {
    // κ΄€λ ¨ 데이터 μ‚­μ œ
    await PostModel.del(
      (await PostModel.findMany("A", { 
        user_id: userId, 
        num: 0 
      })).rows.map(p => p.id)
    );
    
    // μ‚¬μš©μž μ‚­μ œ
    await UserModel.del([userId]);
    
    // λͺ¨λ‘ μ„±κ³΅ν•˜κ±°λ‚˜ λͺ¨λ‘ μ‹€νŒ¨
    return { success: true };
  }
}

κΆŒν•œ 확인

기본적으둜 del은 κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€.
@api({ 
  httpMethod: "POST", 
  clients: ["axios", "tanstack-mutation"], 
  guards: ["admin"]  // κΈ°λ³Έ μ„€μ •
})
async del(ids: number[]): Promise<number> {
  // ...
}

본인 데이터 μ‚­μ œ

import { Sonamu, api, UnauthorizedException } from "sonamu";

class PostFrame {
  @api({ httpMethod: "POST" })
  async deleteMyPost(postId: number) {
    const { user } = Sonamu.getContext();
    
    // κ²Œμ‹œλ¬Ό μ†Œμœ μž 확인
    const post = await PostModel.findById("A", postId);
    
    if (post.user_id !== user.id) {
      throw new UnauthorizedException("본인의 κ²Œμ‹œλ¬Όλ§Œ μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€");
    }
    
    await PostModel.del([postId]);
    
    return { success: true };
  }
}

μ‹€μ „ μ˜ˆμ‹œ

import { UserModel, PostModel, CommentModel } from "./models";
import { Sonamu, api, transactional } from "sonamu";

class UserFrame {
  @api({ httpMethod: "POST" })
  @transactional()
  async deleteAccount() {
    const { user } = Sonamu.getContext();
    
    // μ‚¬μš©μžμ˜ λͺ¨λ“  λŒ“κΈ€ μ‚­μ œ
    const { rows: comments } = await CommentModel.findMany("A", {
      user_id: user.id,
      num: 0
    });
    if (comments.length > 0) {
      await CommentModel.del(comments.map(c => c.id));
    }
    
    // μ‚¬μš©μžμ˜ λͺ¨λ“  κ²Œμ‹œλ¬Ό μ‚­μ œ
    const { rows: posts } = await PostModel.findMany("A", {
      user_id: user.id,
      num: 0
    });
    if (posts.length > 0) {
      await PostModel.del(posts.map(p => p.id));
    }
    
    // μ‚¬μš©μž μ‚­μ œ
    await UserModel.del([user.id]);
    
    // μ„Έμ…˜ μ’…λ£Œ
    Sonamu.getContext().passport.logout();
    
    return { success: true };
  }
}

APIμ—μ„œ μ‚¬μš©

μžλ™ μƒμ„±λœ del API

// Model 클래슀
class UserModelClass extends BaseModelClass {
  @api({ 
    httpMethod: "POST", 
    clients: ["axios", "tanstack-mutation"], 
    guards: ["admin"] 
  })
  async del(ids: number[]): Promise<number> {
    // μžλ™ μƒμ„±λœ μ½”λ“œ
  }
}

ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œ

import { UserService } from "@/services/UserService";

// μ‚¬μš©μž μ‚­μ œ
const count = await UserService.del([1, 2, 3]);
console.log(`${count} users deleted`);

React (TanStack Query)

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { UserService } from "@/services/UserService";

function DeleteUserButton({ userId }: { userId: number }) {
  const queryClient = useQueryClient();
  
  const deleteUser = useMutation({
    mutationFn: (id: number) => UserService.del([id]),
    onSuccess: () => {
      // μΊμ‹œ λ¬΄νš¨ν™”
      queryClient.invalidateQueries({ queryKey: ["users"] });
    }
  });

  const handleDelete = () => {
    if (confirm("정말 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?")) {
      deleteUser.mutate(userId);
    }
  };

  return (
    <button 
      onClick={handleDelete}
      disabled={deleteUser.isPending}
    >
      {deleteUser.isPending ? "Deleting..." : "Delete"}
    </button>
  );
}

μ™Έλž˜ ν‚€ μ œμ•½μ‘°κ±΄

CASCADE μ„€μ •

-- λΆ€λͺ¨ λ ˆμ½”λ“œ μ‚­μ œ μ‹œ μžμ‹ λ ˆμ½”λ“œ μžλ™ μ‚­μ œ
CREATE TABLE comments (
  id SERIAL PRIMARY KEY,
  post_id INTEGER NOT NULL,
  content TEXT NOT NULL,
  FOREIGN KEY (post_id) 
    REFERENCES posts(id) 
    ON DELETE CASCADE
);
// κ²Œμ‹œλ¬Ό μ‚­μ œ μ‹œ λŒ“κΈ€ μžλ™ μ‚­μ œλ¨
await PostModel.del([1]);

RESTRICT μ„€μ •

-- μžμ‹ λ ˆμ½”λ“œκ°€ 있으면 λΆ€λͺ¨ λ ˆμ½”λ“œ μ‚­μ œ λΆˆκ°€
CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL,
  FOREIGN KEY (user_id) 
    REFERENCES users(id) 
    ON DELETE RESTRICT
);
// κ²Œμ‹œλ¬Όμ΄ μžˆλŠ” μ‚¬μš©μž μ‚­μ œ μ‹œ μ—λŸ¬
try {
  await UserModel.del([1]);
} catch (error) {
  // Foreign key violation
  console.error("Cannot delete user with posts");
}

μˆ˜λ™ 처리

@transactional()
async deleteUserWithPosts(userId: number) {
  // λ¨Όμ € κ²Œμ‹œλ¬Ό μ‚­μ œ
  const { rows: posts } = await PostModel.findMany("A", {
    user_id: userId,
    num: 0
  });
  
  if (posts.length > 0) {
    await PostModel.del(posts.map(p => p.id));
  }
  
  // κ·Έ λ‹€μŒ μ‚¬μš©μž μ‚­μ œ
  await UserModel.del([userId]);
  
  return { success: true };
}

μ‚­μ œ 확인

쑴재 μ—¬λΆ€ 확인

async deleteUserSafely(userId: number) {
  // μ‚¬μš©μž 쑴재 확인
  try {
    await UserModel.findById("A", userId);
  } catch (error) {
    if (error instanceof NotFoundException) {
      throw new BadRequestException("μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€");
    }
    throw error;
  }
  
  // μ‚­μ œ
  const count = await UserModel.del([userId]);
  
  return { success: count > 0 };
}

μ‚­μ œ ν›„ 확인

async deleteAndVerify(userId: number) {
  // μ‚­μ œ
  const count = await UserModel.del([userId]);
  
  // μ‹€μ œλ‘œ μ‚­μ œλ˜μ—ˆλŠ”μ§€ 확인
  const deleted = await UserModel.findOne("A", { id: userId });
  
  return {
    deleteCount: count,
    verified: deleted === null
  };
}

μ„±λŠ₯ μ΅œμ ν™”

배치 μ‚­μ œ

λŒ€λŸ‰ μ‚­μ œ μ‹œ 배치둜 λ‚˜λˆ μ„œ μ²˜λ¦¬ν•˜μ„Έμš”.
async deleteManyUsers(ids: number[]) {
  const batchSize = 500;
  let totalDeleted = 0;
  
  for (let i = 0; i < ids.length; i += batchSize) {
    const batch = ids.slice(i, i + batchSize);
    const count = await UserModel.del(batch);
    totalDeleted += count;
  }
  
  return { total: totalDeleted };
}

인덱슀 ν™œμš©

자주 μ‚­μ œν•˜λŠ” μ™Έλž˜ 킀에 인덱슀λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.
-- user_id둜 자주 μ‚­μ œν•˜λŠ” 경우
CREATE INDEX idx_posts_user_id ON posts(user_id);

μ£Όμ˜μ‚¬ν•­

1. λ°°μ—΄λ‘œ 전달

del은 λ°˜λ“œμ‹œ 배열을 λ°›μŠ΅λ‹ˆλ‹€.
// ❌ 잘λͺ»λ¨
await UserModel.del(123);

// βœ… μ˜¬λ°”λ¦„
await UserModel.del([123]);

2. λ°˜ν™˜κ°’μ€ 개수

const count = await UserModel.del([1, 2, 3]);
console.log(count);  // 3 (λ˜λŠ” μ‹€μ œ μ‚­μ œλœ 개수)

3. CASCADE 주의

CASCADE 섀정이 있으면 μ˜λ„μΉ˜ μ•Šμ€ λ°μ΄ν„°κΉŒμ§€ μ‚­μ œλ  수 μžˆμŠ΅λ‹ˆλ‹€.
// ❌ μœ„ν—˜: κ²Œμ‹œλ¬Όμ˜ λͺ¨λ“  λŒ“κΈ€λ„ μ‚­μ œλ¨
await PostModel.del([1, 2, 3]);

4. νŠΈλžœμž­μ…˜ ꢌμž₯

κ΄€λ ¨λœ μ—¬λŸ¬ ν…Œμ΄λΈ”μ„ μ‚­μ œν•  λ•ŒλŠ” νŠΈλžœμž­μ…˜μ„ μ‚¬μš©ν•˜μ„Έμš”.
// βœ… μ•ˆμ „: λͺ¨λ‘ μ„±κ³΅ν•˜κ±°λ‚˜ λͺ¨λ‘ μ‹€νŒ¨
@transactional()
async deleteUserAndRelated(userId: number) {
  await CommentModel.del([...]);
  await PostModel.del([...]);
  await UserModel.del([userId]);
}

5. κΆŒν•œ 확인

κΈ°λ³Έ guards: ["admin"] 섀정을 λ³€κ²½ν•  λ•ŒλŠ” μ‹ μ€‘ν•˜μ„Έμš”.
// ⚠️ 주의: λˆ„κ΅¬λ‚˜ μ‚­μ œ κ°€λŠ₯
@api({ guards: [] })
async del(ids: number[]) {
  // μœ„ν—˜!
}

μ†Œν”„νŠΈ μ‚­μ œ vs ν•˜λ“œ μ‚­μ œ

ν•˜λ“œ μ‚­μ œ (del)

// 영ꡬ적으둜 DBμ—μ„œ 제거
await UserModel.del([1]);
μž₯점:
  • λ””μŠ€ν¬ 곡간 μ ˆμ•½
  • λ‹¨μˆœν•œ ꡬ쑰
단점:
  • 볡ꡬ λΆˆκ°€λŠ₯
  • νžˆμŠ€ν† λ¦¬ 손싀

μ†Œν”„νŠΈ μ‚­μ œ (save)

// status λ˜λŠ” deleted_at 컬럼 μ‚¬μš©
await UserModel.save([
  {
    id: 1,
    status: "deleted",
    deleted_at: new Date()
  }
]);
μž₯점:
  • 볡ꡬ κ°€λŠ₯
  • νžˆμŠ€ν† λ¦¬ μœ μ§€
  • 감사 좔적
단점:
  • λ””μŠ€ν¬ 곡간 증가
  • 쿼리 λ³΅μž‘λ„ 증가

λ‹€μŒ 단계