diff --git a/backend/Dockerfile b/backend/Dockerfile index f3f4c04..f7d2e4a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,4 +4,4 @@ WORKDIR /app COPY build/libs/*.jar app.jar -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-jar", "app.jar"] \ No newline at end of file diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java b/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java index 0347795..37f18ba 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java @@ -225,4 +225,55 @@ public record UnderstandingCheckCreateResponse( LocalDateTime createdAt ) { } + + // 질문 등록 시 SSE로 내려가는 이벤트. 같은 세션 질문방을 보고 있는 모든 클라이언트에게 전파된다. + public record QuestionCreatedEvent( + String type, + Long sessionId, + Long questionId, + String content, + String imageUrl, + // 좋아요 수 (생성 직후에는 0) + Integer likeCount, + // 댓글 수 (생성 직후에는 0) + Integer commentCount, + LocalDateTime createdAt + ) { + } + + // 운영진이 이해도 체크를 생성했을 때 SSE로 내려가는 이벤트. + // 같은 세션 질문방을 보고 있는 모든 클라이언트에게 전파된다. + public record UnderstandingCheckCreatedEvent( + String type, + Long sessionId, + Long checkId, + String content, + // 생성 직후에는 0 + Integer respondedCount, + // 해당 세션의 출석 인원 (이해도 분모) + Integer attendanceCount, + // 생성 직후에는 0 + Integer understoodCount, + // 생성 직후에는 0 + Integer notUnderstoodCount, + LocalDateTime createdAt + ) { + } + + // 누군가 이해도 O/X를 누를 때 SSE로 내려가는 이벤트. + // 같은 세션 질문방을 보고 있는 모든 클라이언트의 이해도 카운트를 실시간으로 갱신한다. + public record UnderstandingResponseUpdatedEvent( + String type, + Long sessionId, + Long checkId, + // 화면의 \"13/29\" 중 13: O 응답 수 + X 응답 수 + Integer respondedCount, + // 화면의 \"13/29\" 중 29: 해당 세션에 대응되는 출석 회차의 출석 인원 + Integer attendanceCount, + // 오른쪽 O 뱃지 숫자 + Integer understoodCount, + // 오른쪽 X 뱃지 숫자 + Integer notUnderstoodCount + ) { + } } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/repository/QuestionAnonymousIdentityRepository.java b/backend/src/main/java/com/example/Piroin/project/domain/question/repository/QuestionAnonymousIdentityRepository.java index cab03c1..53448d9 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/repository/QuestionAnonymousIdentityRepository.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/repository/QuestionAnonymousIdentityRepository.java @@ -5,6 +5,8 @@ import com.example.Piroin.project.domain.user.entity.User; import com.example.Piroin.project.domain.user.enums.Role; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; @@ -18,4 +20,10 @@ public interface QuestionAnonymousIdentityRepository extends JpaRepository emitters = sessionEmitters.getOrDefault(sessionId, List.of()); for (SseEmitter emitter : emitters) { try { emitter.send(SseEmitter.event() - .name("comment-created") - .data(event)); + .name(eventName) + .data(data)); } catch (IOException | IllegalStateException e) { removeEmitter(sessionId, emitter); emitter.completeWithError(e); diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java b/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java index 0d47992..a1aca36 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java @@ -196,7 +196,7 @@ private String assignAnonymousIdentity(Question question, User commenter) { .orElseGet(() -> { // 처음 댓글 다는 유저 → 역할별 카운트 기반으로 새 번호 부여 int nextNo = anonymousIdentityRepository - .countByQuestionAndUser_Role(question, commenter.getRole()) + 1; + .findMaxAnonymousNoByQuestionAndRole(question, commenter.getRole()) + 1; anonymousIdentityRepository.save(QuestionAnonymousIdentity.builder() .question(question) @@ -242,7 +242,12 @@ public QuestionResDTO.CreateRes createQuestion(Long sessionId, QuestionReqDTO.Cr .updatedAt(LocalDateTime.now()) .build(); - return QuestionResDTO.CreateRes.from(questionRepository.save(question)); + Question saved = questionRepository.save(question); + + // DB 반영 후 같은 세션을 보고 있는 모든 클라이언트에게 새 질문을 알림 + publishQuestionCreatedEventAfterCommit(saved); + + return QuestionResDTO.CreateRes.from(saved); } // 좋아요 토글 @@ -375,6 +380,11 @@ public QuestionResDTO.UnderstandingCheckCreateResponse createUnderstandingCheck( .updatedAt(now) .build()); + int attendanceCount = attendanceService.countAttendedBySession(session); + + // DB 반영 후 같은 세션을 보고 있는 모든 클라이언트에게 새 이해도 체크를 알림 + publishUnderstandingCheckCreatedEventAfterCommit(session.getId(), check, attendanceCount); + return new QuestionResDTO.UnderstandingCheckCreateResponse( check.getId(), check.getTitle(), 0, null, 0, 0, check.getCreatedAt() ); @@ -396,7 +406,12 @@ public QuestionResDTO.UnderstandingResponseResult respondUnderstandingCheck( UnderstandResChoice selectedChoice = applyUnderstandingResponse(check, loginUser, request.getChoice()); // O/X 클릭 직후 프론트가 13/29와 O/X 뱃지를 바로 갱신할 수 있도록 최신 분모도 함께 내려준다. int attendanceCount = attendanceService.countAttendedBySession(session); - return toUnderstandingResponseResult(check, selectedChoice, attendanceCount); + QuestionResDTO.UnderstandingResponseResult result = toUnderstandingResponseResult(check, selectedChoice, attendanceCount); + + // DB 반영 후 같은 세션을 보고 있는 모든 클라이언트의 이해도 카운트를 갱신 + publishUnderstandingResponseUpdatedEventAfterCommit(sessionId, result); + + return result; } // 공통 헬퍼 메서드 @@ -677,6 +692,57 @@ private void publishCommentCreatedEventAfterCommit(Question question) { publishAfterCommit(() -> questionEventService.publishCommentCreated(sessionId, event)); } + private void publishQuestionCreatedEventAfterCommit(Question question) { + Long sessionId = question.getSession().getId(); + + QuestionResDTO.QuestionCreatedEvent event = new QuestionResDTO.QuestionCreatedEvent( + "QUESTION_CREATED", + sessionId, + question.getId(), + question.getContent(), + question.getImageUrl(), + question.getLikeCount(), + 0, // 방금 만들어진 질문이므로 댓글 수는 0 + question.getCreatedAt() + ); + + publishAfterCommit(() -> questionEventService.publishQuestionCreated(sessionId, event)); + } + + private void publishUnderstandingCheckCreatedEventAfterCommit( + Long sessionId, UnderstandingCheck check, int attendanceCount + ) { + QuestionResDTO.UnderstandingCheckCreatedEvent event = new QuestionResDTO.UnderstandingCheckCreatedEvent( + "UNDERSTANDING_CHECK_CREATED", + sessionId, + check.getId(), + check.getTitle(), + 0, // 생성 직후 응답 수 0 + attendanceCount, + 0, // 생성 직후 O 0 + 0, // 생성 직후 X 0 + check.getCreatedAt() + ); + + publishAfterCommit(() -> questionEventService.publishUnderstandingCheckCreated(sessionId, event)); + } + + private void publishUnderstandingResponseUpdatedEventAfterCommit( + Long sessionId, QuestionResDTO.UnderstandingResponseResult result + ) { + QuestionResDTO.UnderstandingResponseUpdatedEvent event = new QuestionResDTO.UnderstandingResponseUpdatedEvent( + "UNDERSTANDING_RESPONSE_UPDATED", + sessionId, + result.checkId(), + result.respondedCount(), + result.attendanceCount(), + result.understoodCount(), + result.notUnderstoodCount() + ); + + publishAfterCommit(() -> questionEventService.publishUnderstandingResponseUpdated(sessionId, event)); + } + // 롤백된 댓글이 실시간 화면에 먼저 보이지 않도록, 활성화된 트랜잭션 동기화 안에서만 커밋 이후 이벤트를 발행한다. private void publishAfterCommit(Runnable action) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 11c99f9..a83439d 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -19,6 +19,11 @@ spring: hibernate: format_sql: true packagesToScan: com.example.Piroin.project.domain + jdbc: + time_zone: Asia/Seoul + + jackson: + time-zone: Asia/Seoul jwt: secret: ${JWT_SECRET} @@ -32,3 +37,11 @@ management: file: upload-dir: uploads/ + +# SSE 연결은 HTTP 커넥션을 끊지 않고 유지 +# 연결당 Tomcat 스레드 하나를 점유하므로, 동시 접속자 수를 고려해 스레드 수를 넉넉하게 설정 +server: + tomcat: + threads: + max: 200 # 기본값 200 (명시적으로 설정) + min-spare: 20 # 최소 대기 스레드 수 diff --git a/backend/src/main/resources/db/migration/V5__add_unique_constraint_question_anonymous_identity.sql b/backend/src/main/resources/db/migration/V5__add_unique_constraint_question_anonymous_identity.sql new file mode 100644 index 0000000..5fbcf3e --- /dev/null +++ b/backend/src/main/resources/db/migration/V5__add_unique_constraint_question_anonymous_identity.sql @@ -0,0 +1,3 @@ +ALTER TABLE question_anonymous_identity + ADD CONSTRAINT uq_question_anonymous_identity_question_user + UNIQUE (question_id, user_id); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6e43dfa..1b6c692 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -68,6 +68,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -717,6 +718,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, @@ -1600,6 +1602,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-module-imports": "^7.28.6", @@ -3342,6 +3345,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3819,6 +3823,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -3872,6 +3877,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4241,6 +4247,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4339,6 +4346,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5293,6 +5301,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -7126,6 +7135,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9892,6 +9902,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -10789,6 +10800,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12156,6 +12168,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13290,6 +13303,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13667,6 +13681,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13798,6 +13813,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13831,6 +13847,7 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14329,6 +14346,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -14571,6 +14589,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15941,6 +15960,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16109,6 +16129,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -16538,6 +16559,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -16608,6 +16630,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -17029,6 +17052,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000..1279485 Binary files /dev/null and b/frontend/public/favicon.png differ diff --git a/frontend/public/index.html b/frontend/public/index.html index aa069f2..c504689 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -2,7 +2,7 @@ - + - React App + PIROIN diff --git a/frontend/src/pages/OnboardingPage.js b/frontend/src/pages/OnboardingPage.js index 933057d..20ca746 100644 --- a/frontend/src/pages/OnboardingPage.js +++ b/frontend/src/pages/OnboardingPage.js @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import styles from './OnboardingPage.module.css'; import logo from '../assets/images/logo.png'; @@ -7,6 +7,8 @@ function OnboardingPage() { const navigate = useNavigate(); useEffect(() => { + document.title = "PIROIN"; + const timer = setTimeout(() => { const token = localStorage.getItem('token'); if (token) { diff --git a/frontend/src/pages/curriculum/CurriculumPage.js b/frontend/src/pages/curriculum/CurriculumPage.js index 656f529..d369939 100644 --- a/frontend/src/pages/curriculum/CurriculumPage.js +++ b/frontend/src/pages/curriculum/CurriculumPage.js @@ -323,6 +323,10 @@ function CurriculumPage() { return acc; }, {}); + useEffect(() => { + document.title = "커리큘럼 | PIROIN"; + }, []); + return (
{role === 'ADMIN' && ( diff --git a/frontend/src/pages/login/LoginPage.js b/frontend/src/pages/login/LoginPage.js index 91a2eeb..7fbd50d 100644 --- a/frontend/src/pages/login/LoginPage.js +++ b/frontend/src/pages/login/LoginPage.js @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { authFetch } from '../../utils/Api'; import styles from './LoginPage.module.css'; @@ -37,6 +37,10 @@ function LoginPage() { } }; + useEffect(() => { + document.title = "로그인 | PIROIN"; + }, []); + return (

PIROIN

diff --git a/frontend/src/pages/pirocheck/PIroCheckMain.js b/frontend/src/pages/pirocheck/PIroCheckMain.js index 8faf3a2..092f8c5 100644 --- a/frontend/src/pages/pirocheck/PIroCheckMain.js +++ b/frontend/src/pages/pirocheck/PIroCheckMain.js @@ -1,5 +1,6 @@ import { useNavigate } from 'react-router-dom'; import styles from './PIroCheckMain.module.css'; +import { useState, useEffect } from 'react'; function PIroCheckMain() { const navigate = useNavigate(); @@ -19,6 +20,10 @@ function PIroCheckMain() { const menus = role === 'ADMIN' ? adminMenus : memberMenus; + useEffect(() => { + document.title = "피로체크 | PIROIN"; + }, []); + return (
{menus.map((menu, i) => ( diff --git a/frontend/src/pages/qna/QnADetailPage.js b/frontend/src/pages/qna/QnADetailPage.js index a23ba91..d5c8d23 100644 --- a/frontend/src/pages/qna/QnADetailPage.js +++ b/frontend/src/pages/qna/QnADetailPage.js @@ -52,6 +52,8 @@ function QnADetailPage() { // ── 질문 불러오기 ──────────────────────────────── useEffect(() => { + document.title = "Q&A | PIROIN"; + const fetchQuestion = async () => { try { setLoading(true); diff --git a/frontend/src/pages/qna/QnAListPage.js b/frontend/src/pages/qna/QnAListPage.js index 5afae11..635d97c 100644 --- a/frontend/src/pages/qna/QnAListPage.js +++ b/frontend/src/pages/qna/QnAListPage.js @@ -336,6 +336,10 @@ function QnAListPage() { const currentChoice = myChoices[understanding?.current?.checkId]; + useEffect(() => { + document.title = "Q&A | PIROIN"; + }, []); + return (

{sessionTitle}

diff --git a/frontend/src/pages/qna/QnAMainPage.js b/frontend/src/pages/qna/QnAMainPage.js index db1dcb2..15c039e 100644 --- a/frontend/src/pages/qna/QnAMainPage.js +++ b/frontend/src/pages/qna/QnAMainPage.js @@ -17,6 +17,8 @@ function QNAMainPage() { // ── 세션 목록 불러오기 ────────────────────────── useEffect(() => { + document.title = "Q&A | PIROIN"; + const fetchSessions = async () => { try { setLoading(true);