커뮤니티 가입/탈퇴하기 기능 구현

파일 구조

  1. community.controller.js
  2. community.service.js
  3. community.repository.js

community.controller.js


/**
 * @swagger
 * /api/community/type/join:
 *   post:
 *     summary: 커뮤니티 가입 또는 탈퇴
 *     description: 로그인된 사용자가 특정 커뮤니티에 가입하거나 탈퇴합니다. 가입 시 프로필 타입(BASIC/MULTI) 선택 가능하며, MULTI 선택 시 멀티 프로필을 즉시 생성합니다.
 *     tags: [Community]
 *     security: [{ bearerAuth: [] }]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required: [communityId, action]
 *             properties:
 *               communityId: { type: integer, example: 3 }
 *               action:
 *                 type: string
 *                 enum: [join, leave]
 *                 example: join
 *               profileType:
 *                 type: string
 *                 description: 가입 시 사용할 프로필 타입 (join일 때만 사용)
 *                 enum: [BASIC, MULTI]
 *                 example: BASIC
 *               multi:
 *                 type: object
 *                 nullable: true
 *                 description: profileType=MULTI일 때 생성할 멀티 프로필 정보
 *                 properties:
 *                   nickname: { type: string, example: "뮤지컬덕후" }
 *                   image: { type: string, nullable: true, example: "https://example.com/image.png" }
 *                   bio: { type: string, nullable: true, example: "배우 덕질은 삶의 활력" }
 *     responses:
 *       200:
 *         description: 처리 성공
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success: { type: boolean, example: true }
 *                 message: { type: string, example: "커뮤니티 가입 완료" }
 *       400:
 *         description: 잘못된 요청 또는 처리 실패
 */

router.post("/type/join", authenticateJWT, async (req, res) => {
  try {
    const userId = req.user?.id;
    const { communityId, action, profileType, multi } = req.body;
    if (!userId || !communityId || !["join", "leave"].includes(action)) {
      return res.status(400).json({
        success: false,
        message: "userId, communityId, action(join/leave)을 확인하세요.",
      });
    }
    const message = await handleJoinOrLeaveCommunity(
      userId,
      Number(communityId),
      action,
      profileType,
      multi
    );
    res.status(200).json({ success: true, message });
  } catch (error) {
    res.status(400).json({ success: false, message: error.message });
  }
});
  

community.service.js


export const handleJoinOrLeaveCommunity = async (
  userId,
  communityId,
  action,
  profileType,
  multi
) => {
  const isJoined = await checkUserInCommunity(userId, communityId);

  if (action === "join") {
    if (isJoined) throw new Error("이미 가입된 커뮤니티입니다.");
    
    await prisma.$transaction(async (tx) => {
      await insertUserToCommunity(userId, communityId, tx);
      if (profileType === "MULTI") {
        const dup = await findMultiProfile(communityId, userId, tx);
        if (dup) throw new Error("이미 해당 커뮤니티에 멀티프로필이 있습니다.");
        const ok = await canCreateAnotherMulti(userId);
        if (!ok)
          throw new Error(
            "무료 회원은 멀티프로필을 5개까지 생성할 수 있습니다."
          );
        await createCommunityProfileRepository(
          {
            userId,
            communityId,
            nickname: multi?.nickname ?? "",
            image: multi?.image ?? null,
            bio: multi?.bio ?? null,
          },
          tx
        );
      }
    });
    return "커뮤니티 가입 완료";
  }

  if (action === "leave") {
    if (!isJoined) throw new Error("가입되지 않은 커뮤니티입니다.");
    
    await prisma.$transaction(async (tx) => {
      const mp = await findMultiProfile(communityId, userId, tx);
      if (mp) await deleteCommunityProfileRepository(mp.id, tx);
      await deleteUserFromCommunity(userId, communityId, tx);
    });
    return "커뮤니티 탈퇴 완료";
  }

  throw new Error("유효하지 않은 요청입니다.");
};

// 커뮤니티별 프로필 타입 전환
export const switchCommunityProfileType = async ({
  userId,
  communityId,
  profileType, 
  multi, 
}) => {
  return await prisma.$transaction(async (tx) => {
    const current = await findMultiProfile(communityId, userId, tx);
    const isMulti = !!current;

    if (profileType === "BASIC") {
      if (isMulti) await deleteCommunityProfileRepository(current.id, tx);
      return { changedTo: "BASIC" };
    }

    if (profileType === "MULTI") {
      if (isMulti) throw new Error("이미 멀티프로필을 사용 중입니다.");
      const ok = await canCreateAnotherMulti(userId);
      if (!ok)
        throw new Error("무료 회원은 멀티프로필을 5개까지 생성할 수 있습니다.");
      const created = await createCommunityProfileRepository(
        {
          userId,
          communityId,
          nickname: multi?.nickname ?? "",
          image: multi?.image ?? null,
          bio: multi?.bio ?? null,
        },
        tx
      );
      return { changedTo: "MULTI", profile: created };
    }

    throw new Error("profileType은 BASIC 또는 MULTI여야 합니다.");
  });
};
  

community.repository.js


export const checkUserInCommunity = async (
  userId,
  communityId,
  db = prisma
) => {
  const record = await db.userCommunity.findFirst({
    where: {
      userId,
      communityId,
    },
  });
  return !!record;
};
  

돌아가기: 기능별 API 구현 내용 보러가기