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
131 changes: 131 additions & 0 deletions src/controllers/script.controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { scriptResponseDTO, scriptVersionResponseDTO } from "../dtos/script.dto.js";
import {
processBulkEditProjectScripts,
processGetProjectScripts,
processScriptGet,
processScriptRestore,
processScriptUpdate,
Expand Down Expand Up @@ -248,3 +250,132 @@ export const handleRestoreVersion = async (req, res, next) => {
next(error);
}
};

/**
* @swagger
* /presentations/{projectId}/scripts:
* get:
* summary: 프로젝트 전체 대본 조회
* description: 프로젝트의 슬라이드 순서대로 현재 대본 목록을 조회합니다.
* tags: [Script]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* example: "123"
* description: 대본을 가져올 프로젝트 ID
* responses:
* 200:
* description: 프로젝트 대본 조회 성공
* content:
* application/json:
* example:
* resultType: "SUCCESS"
* error: null
* success:
* message: "프로젝트 대본이 성공적으로 조회되었습니다."
* projectId: "123"
* scripts:
* - slideId: "1"
* scriptText: "첫 번째 슬라이드 대본"
* - slideId: "2"
* scriptText: ""
*/
export const handleGetProjectScripts = async (req, res, next) => {
try {
const { projectId } = req.params;
const userId = req.user.id;
const result = await processGetProjectScripts({ projectId, userId });

res.status(200).json({
resultType: "SUCCESS",
error: null,
success: {
message: "프로젝트 대본이 성공적으로 조회되었습니다.",
...result,
},
});
} catch (error) {
next(error);
}
};

/**
* @swagger
* /presentations/{projectId}/scripts/bulk-edit:
* patch:
* summary: 프로젝트 대본 일괄 수정
* description: 프로젝트에 속한 여러 슬라이드의 대본을 한 번에 저장합니다.
* tags: [Script]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* example: "123"
* description: 대본을 수정할 프로젝트 ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - scripts
* properties:
* scripts:
* type: array
* items:
* type: object
* required:
* - slideId
* - scriptText
* properties:
* slideId:
* type: string
* example: "1"
* scriptText:
* type: string
* example: "수정된 대본입니다."
* responses:
* 200:
* description: 프로젝트 대본 일괄 수정 성공
* content:
* application/json:
* example:
* resultType: "SUCCESS"
* error: null
* success:
* message: "대본 일괄 수정이 완료되었습니다."
* projectId: "123"
* requestedSlideCount: 2
* updatedSlideCount: 1
* unchangedSlideCount: 1
* updatedSlideIds: ["1"]
*/
export const handleBulkEditProjectScripts = async (req, res, next) => {
try {
const { projectId } = req.params;
const userId = req.user.id;
const { scripts } = req.body;
const result = await processBulkEditProjectScripts({ projectId, userId, scripts });

res.status(200).json({
resultType: "SUCCESS",
error: null,
success: {
message: "대본 일괄 수정이 완료되었습니다.",
...result,
},
});
} catch (error) {
next(error);
}
};
18 changes: 18 additions & 0 deletions src/errors/script.error.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,21 @@ export class VersionNotFoundError extends BaseError {
super("버전이 존재하지 않습니다.", 404, "SC002", data);
}
}

export class ScriptBulkEditPayloadError extends BaseError {
constructor(data) {
super("유효한 대본 목록이 필요합니다.", 400, "SC003", data);
}
}

export class ScriptBulkEditDuplicateSlideError extends BaseError {
constructor(data) {
super("중복된 슬라이드 ID가 포함되어 있습니다.", 400, "SC004", data);
}
}

export class ScriptBulkEditSlideNotFoundError extends BaseError {
constructor(data) {
super("프로젝트에 속하지 않은 슬라이드가 포함되어 있습니다.", 400, "SC005", data);
}
}
29 changes: 29 additions & 0 deletions src/repositories/script.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,35 @@ export const getScriptVersionList = async (slideId) => {
});
};

// 프로젝트 슬라이드/대본 목록 조회 (일괄 수정용)
export const getProjectSlidesWithScripts = async (projectId, userId) => {
return await prisma.project.findFirst({
where: {
id: BigInt(projectId),
userId: BigInt(userId),
isDeleted: false,
},
select: {
id: true,
slides: {
where: {
isDeleted: false,
},
select: {
id: true,
slideNum: true,
script: {
select: {
scriptText: true,
},
},
},
orderBy: [{ slideNum: "asc" }, { id: "asc" }],
},
},
});
};

// 대본 버전 복원
export const postScriptVersion = async (slideId, versionNumber) => {
return await prisma.$transaction(async (tx) => {
Expand Down
10 changes: 10 additions & 0 deletions src/routes/project.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
handleGetProjectName,
handleUpdateProjectName,
} from "../controllers/project.controller.js";
import {
handleBulkEditProjectScripts,
handleGetProjectScripts,
} from "../controllers/script.controller.js";
import {
handleGetRecentComments,
handleGetSlideAnalytics,
Expand All @@ -22,6 +26,12 @@ router.post("/", isLogin, handleCreateProject);
// 프로젝트 목록 조회/검색
router.get("/", isLogin, handleGetProjectList);

// 프로젝트 전체 대본 조회
router.get("/:projectId/scripts", isLogin, handleGetProjectScripts);

// 프로젝트 대본 일괄 수정
router.patch("/:projectId/scripts/bulk-edit", isLogin, handleBulkEditProjectScripts);

// 프로젝트 이름 업데이트
router.patch("/:projectId", isLogin, handleUpdateProjectName);

Expand Down
104 changes: 104 additions & 0 deletions src/services/script.service.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import {
getProjectSlidesWithScripts,
getScriptText,
getScriptVersionList,
postScriptVersion,
updateScriptText,
} from "../repositories/script.repository.js";
import { ProjectNotFoundError } from "../errors/project.error.js";
import {
ScriptBulkEditDuplicateSlideError,
ScriptBulkEditPayloadError,
ScriptBulkEditSlideNotFoundError,
} from "../errors/script.error.js";

export const processScriptUpdate = async (slideId, text) => {
try {
Expand Down Expand Up @@ -76,3 +83,100 @@ export const processScriptRestore = async (id, versionNumber) => {
throw error;
}
};

const normalizeBulkEditScripts = (scripts) => {
if (!Array.isArray(scripts) || scripts.length < 1) {
throw new ScriptBulkEditPayloadError({ scripts });
}

const normalized = [];
const usedSlideIds = new Set();

for (let i = 0; i < scripts.length; i++) {
const item = scripts[i];
if (!item || typeof item !== "object") {
throw new ScriptBulkEditPayloadError({ index: i, item });
}

const slideId = item.slideId != null ? String(item.slideId) : "";
if (!slideId || !/^\d+$/.test(slideId)) {
throw new ScriptBulkEditPayloadError({ index: i, slideId: item.slideId });
}

if (usedSlideIds.has(slideId)) {
throw new ScriptBulkEditDuplicateSlideError({ slideId });
}

if (typeof item.scriptText !== "string") {
throw new ScriptBulkEditPayloadError({ index: i, slideId, scriptText: item.scriptText });
}

usedSlideIds.add(slideId);
normalized.push({ slideId, scriptText: item.scriptText });
}

return normalized;
};

export const processGetProjectScripts = async ({ projectId, userId }) => {
const project = await getProjectSlidesWithScripts(projectId, userId);
if (!project) {
throw new ProjectNotFoundError({ projectId });
}

return {
projectId: project.id.toString(),
scripts: (project.slides || []).map((slide) => ({
slideId: slide.id.toString(),
scriptText: slide.script?.scriptText || "",
})),
};
};

export const processBulkEditProjectScripts = async ({ projectId, userId, scripts }) => {
const normalizedScripts = normalizeBulkEditScripts(scripts);

const project = await getProjectSlidesWithScripts(projectId, userId);
if (!project) {
throw new ProjectNotFoundError({ projectId });
}

const projectSlideIds = new Set((project.slides || []).map((slide) => slide.id.toString()));
const invalidSlideIds = normalizedScripts
.filter((item) => !projectSlideIds.has(item.slideId))
.map((item) => item.slideId);

if (invalidSlideIds.length > 0) {
throw new ScriptBulkEditSlideNotFoundError({
projectId,
slideIds: invalidSlideIds,
});
}

let updatedSlideCount = 0;
let unchangedSlideCount = 0;
const updatedSlideIds = [];

const updateResults = await Promise.all(
normalizedScripts.map((item) => processScriptUpdate(item.slideId, item.scriptText)),
);

for (let i = 0; i < updateResults.length; i++) {
const { isUpdated } = updateResults[i];
if (isUpdated) {
updatedSlideCount += 1;
updatedSlideIds.push(normalizedScripts[i].slideId);
continue;
}

unchangedSlideCount += 1;
}

return {
projectId: project.id.toString(),
requestedSlideCount: normalizedScripts.length,
updatedSlideCount,
unchangedSlideCount,
updatedSlideIds,
};
};
Loading