Skip to content

Develop#13

Merged
choihooo merged 6 commits intomainfrom
develop
Jan 25, 2026
Merged

Develop#13
choihooo merged 6 commits intomainfrom
develop

Conversation

@choihooo
Copy link
Member

@choihooo choihooo commented Jan 25, 2026

📝 변경 사항

🎯 작업 목적

🔍 변경 내용 상세

  • 변경 사항 1
  • 변경 사항 2
  • 변경 사항 3

📸 스크린샷 (선택사항)

✅ 체크리스트

  • 코드 스타일이 프로젝트 가이드라인을 따릅니다
  • 자체 검토를 완료했습니다
  • 주석을 추가했으며, 특히 이해하기 어려운 부분에 주석을 달았습니다
  • 문서를 업데이트했습니다 (필요한 경우)
  • 변경 사항이 새로운 경고를 생성하지 않습니다
  • 테스트를 추가했으며, 기존 테스트를 통과합니다 (필요한 경우)
  • 의존성 변경이 있다면 package.json을 업데이트했습니다

🔗 관련 이슈

Closes #

💬 리뷰어에게 전달할 사항

Summary by CodeRabbit

  • New Features

    • Speech recognition (STT) during call practice, prerecorded in-app audio playback, scenario call scripts, random practice selector, reusable carousel, and a new practice question component.
  • Improvements

    • Improved microphone UI and waveform rendering, streamlined call/conversation flow and submission, safer audio startup via user-gesture handling, and more resilient error handling.
  • Documentation

    • Guides and scripts for generating and organizing audio (macOS/Google TTS) and audio file conventions.

✏️ Tip: You can customize this high-level summary in your review settings.

- 도메인 기반 폴더 구조로 재구성
- 컴포넌트 로직을 커스텀 훅으로 분리 (useCarousel, useRandomPractice)
- 음성 녹음 관련 기능 제거
- Error Boundary 개선
- callScript.ts에 모든 질문의 audioUrl 추가
- iframe 기반 오디오 재생 시스템 구현 (audio-player.html)
- useCallPage 훅에 음성 재생 로직 통합
- 질문 변경 시 자동 음성 재생 기능 추가
- 음성 파일 자동 생성 스크립트 추가 (macOS say, Google TTS API 지원)
- '119'를 '일일구'로 읽히도록 변환 로직 추가
- public/audio/ 디렉토리에 모든 상황별 음성 파일 생성
- .npmrc를 .gitignore에 추가하여 토큰이 커밋되지 않도록 함
- .npmrc에서 하드코딩된 토큰 제거 (환경변수 사용)
@vercel
Copy link

vercel bot commented Jan 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
119-web-client Ready Ready Preview, Comment Jan 25, 2026 1:07pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

📝 Walkthrough

Walkthrough

Adds extensive call/audio features: structured call scripts and public audio assets, iframe-based audio playback, voice-detection + optional STT, multiple call-conversation/timer/submission hooks, carousel/random-practice hooks, UI refactors, audio-generation scripts/docs, and assorted import/path reorganizations.

Changes

Cohort / File(s) Summary
Config & Packaging
'.gitignore', '.npmrc', package.json
.npmrc added to .gitignore; removed GitHub Packages registry/auth lines from .npmrc; added react-error-boundary and speak-tts dependencies.
Carousel & Random CTA
app/(default)/_hooks/useCarousel.ts, app/(default)/_hooks/useRandomPractice.ts, app/(default)/components/EmergencySituationCarousel.tsx, app/(default)/components/RandomPracticeCTA.tsx
New generic useCarousel hook and useRandomPractice hook; components updated to delegate navigation and index state to hooks.
Call scripts & public audio
app/.../dial/call/constants/callScript.ts, public/audio/*, public/audio-player.html, public/audio/AUDIO_FILES_GUIDE.md, public/audio/*/README.md
Added strongly-typed CALL_SCRIPTS + getter, new public audio assets/docs, and an iframe audio player HTML to handle playback via postMessage.
Call orchestration hooks
app/(default)/practice/_hooks/useCallConversation.ts, app/(default)/practice/_hooks/useCallSubmission.ts, app/(default)/practice/_hooks/useCallTimer.ts, app/.../dial/call/hooks/useCallPage.ts
New hooks to manage multi-question conversation, submission to backend (with client IP), call timer, and useCallPage that orchestrates playback, transcript, question flow and report submission.
Voice detection & STT
app/(default)/practice/_hooks/useVoiceDetection.ts, app/.../dial/call/hooks/useVoiceDetection.ts
New/expanded voice-detection hooks integrating web-voice-detection Detect and optional SpeechRecognition STT; lifecycle, callbacks (onSpeechResult/onSpeechEnd), analyser node exposure, and robust cleanup.
Waveform, dial, and call page UI
app/.../dial/call/components/Waveform.tsx, app/.../dial/components/DialPad.tsx, app/.../dial/call/page.tsx, app/.../dial/page.tsx, app/.../dial/call/components/PracticeOptionButton.tsx
Waveform rendering simplified and cleanup clarified; DialPad sets sessionStorage flag before navigation; call/call-page refactored to use useCallPage and new mic states; import paths updated.
Practice content & utils
app/(default)/practice/_components/PracticeQuestionContent.tsx, app/(default)/practice/_utils/situationImage.ts, many .../_constants/... import updates
New PracticeQuestionContent component and getSituationImagePath util; multiple modules now import from reorganized _constants/_utils paths.
Report UI & error handling
app/(report)/report/[reportId]/components/ReportContent.tsx, app/(report)/report/[reportId]/components/ReportErrorBoundary.tsx
Replaced <img> with Next Image; converted class-based error boundary to functional wrapper using react-error-boundary and added default fallback component.
Audio generation scripts & docs
scripts/generate-audio-from-callscript.js, scripts/generate-audio-files.js, scripts/generate-audio-files-simple.js, scripts/*.md
Added scripts to generate audio (macOS say + ffmpeg or Google TTS) from callScript/AUDIO guide, with setup, troubleshooting and README docs.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Call Page UI
    participant Hook as useCallPage
    participant Iframe as Audio Player (iframe)
    participant STT as SpeechRecognition
    participant API as Backend API

    User->>UI: Start call
    UI->>Hook: init/start
    Hook->>Iframe: postMessage { type: "play", url: startAudio }
    Iframe-->>Hook: { type: "ready" }
    Iframe-->>Hook: { type: "playing" }
    Hook-->>UI: show question

    User->>UI: Click record
    UI->>Hook: handleStartRecording()
    Hook->>STT: start recognition
    STT-->>Hook: onSpeechResult (partial)
    STT-->>Hook: onSpeechEnd (final)
    Hook->>Hook: addAnswer(transcript)
    Hook->>Iframe: postMessage { type: "play", url: nextQuestionAudio }

    User->>UI: Finish questions
    UI->>Hook: submitReport()
    Hook->>API: POST /submit (script, clientIP)
    API-->>Hook: { resultId }
    Hook-->>UI: showEndPopup(resultId)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • chore: 배포 수정 #11: Directly touches .gitignore / .npmrc with conflicting changes to whether .npmrc is ignored and registry/auth lines.
  • Develop #10: Overlaps removal of GitHub Packages registry/auth lines in .npmrc.
  • Develop #12: Overlaps call/audio features — useVoiceDetection, Waveform, and call flow changes.

Poem

🐰 I hopped into code with a curious heart,

I stitched scripts and echoes, each audio part,
I framed every question, I tuned the reply,
An iframe hums softly and transcripts fly,
Hop, click, and listen — the practice can start! 🎧

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is entirely a blank template with no concrete information filled in; all required sections are empty or contain placeholder checkboxes. Complete all required sections with actual implementation details: describe changes made, explain the objective, list key modifications, and provide context for reviewers. The commit message indicates IP-related features and other refactoring work not captured in the description.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Develop' is vague and generic, failing to convey any meaningful information about the substantial changes in this pull request. Replace with a descriptive title that summarizes the main changes, such as 'Refactor call flow with hooks and implement audio player integration' or similar.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/(default)/practice/[situationId]/[detailId]/components/PracticeQuestionContent.tsx (1)

3-3: Fix broken import path causing CI failure.

The module path ../../../../constants/practiceQuestions is incorrect. The file is located at app/(default)/practice/_constants/practiceQuestions.ts, so the import should be updated to ../../../_constants/practiceQuestions.

🐛 Proposed fix
-import type { PracticeQuestion } from "../../../../constants/practiceQuestions";
+import type { PracticeQuestion } from "../../../_constants/practiceQuestions";
app/(default)/components/EmergencySituationCarousel.tsx (1)

66-74: Guard against undefined currentItem.

While the empty items guard on line 32 helps, currentItem from the hook could still be undefined if the index becomes stale. Accessing currentItem.tag and currentItem.title would throw at runtime.

🛡️ Suggested defensive check
 	// 안전장치: items가 비어있으면 렌더링 X
-	if (!items || items.length === 0) return null;
+	if (!items || items.length === 0 || !currentItem) return null;
🤖 Fix all issues with AI agents
In `@app/`(default)/_hooks/useCarousel.ts:
- Around line 26-66: The useEffect block watching currentItem (which checks
isInitialMount and calls onSelectRef.current) is redundant because the handlers
handlePrevious, handleNext, and handleDotClick already call onSelect directly,
causing duplicate calls; remove that useEffect (the block referencing
isInitialMount.current, currentItem, and onSelectRef.current) so onSelect is
invoked only once from the handlers, leaving handlePrevious, handleNext,
handleDotClick, and the onSelect prop as the single source of truth for
selection callbacks.
- Around line 14-23: The currentIndex state in the useCarousel hook can become
invalid when the items array shrinks; add a useEffect that watches items (and
optionally items.length) and resets/clamps currentIndex to a valid value (e.g.,
set to 0 if items is empty or set to Math.min(currentIndex, items.length - 1))
so currentItem (items[currentIndex]) never becomes undefined; update
setCurrentIndex inside that effect and reference the hook's currentIndex/items
identifiers to locate where to add this fix.

In `@app/`(default)/_hooks/useRandomPractice.ts:
- Around line 28-35: getRandomSituation currently assumes allDetailSituations
has items and may return undefined; update getRandomSituation (and its caller
navigateToRandomPractice) to guard against an empty array by checking
allDetailSituations.length === 0 and handling that case (e.g., return
null/undefined or throw a controlled error), then in navigateToRandomPractice
check the returned value before accessing selected.situationId/selected.detailId
and bail out or show an error/notification if no selection exists; modify
functions named getRandomSituation and navigateToRandomPractice accordingly so
they safely handle empty allDetailSituations.

In `@app/`(default)/practice/_hooks/useCallConversation.ts:
- Around line 47-58: The flag set by setHasProcessedAnswer(true) in
handleAnswerComplete is not reset when advancing questions, so
hasProcessedAnswer stays true and blocks processing the next answer; update
handleAnswerComplete (the block that calls setCurrentQuestionIndex(nextIndex))
to call resetAnswerProcessed() immediately after advancing the index (or add an
effect that watches currentQuestionIndex and calls resetAnswerProcessed()),
ensuring hasProcessedAnswer is cleared before the next question is presented.

In `@app/`(default)/practice/_hooks/useCallSubmission.ts:
- Around line 18-27: The client-side getClientIP function in
useCallSubmission.ts fetches the IP from ipify.org which raises privacy and
reliability concerns; instead remove the external fetch and obtain the client IP
server-side (e.g., read request headers on the backend endpoint that handles
submissions) and then pass that IP into the useCallSubmission flow or its
submission API payload; update getClientIP to either be removed or to call your
own server endpoint that returns the IP determined from the request (with
fallback behavior removed or documented), and ensure functions interacting with
getClientIP (e.g., submitCall or whatever handler in useCallSubmission) accept
and propagate the server-derived IP rather than calling ipify directly.

In
`@app/`(default)/practice/[situationId]/[detailId]/dial/call/constants/callScript.ts:
- Around line 186-270: Strings in the callScript use inline "<br/>" tags (e.g.,
in hintTitle and question values) which render literally; replace those
occurrences with newline characters ("\n") instead (for example in the objects
for "injury-me", "injury-other", "drowning-friend" and any other situation keys
shown) so hintTitle, question, and similar text fields use "\n" rather than HTML
tags; update all instances consistently in callScript.ts.

In
`@app/`(default)/practice/[situationId]/[detailId]/dial/call/hooks/useCallPage.ts:
- Around line 57-66: In submitReport inside useCallPage.ts (the submitReport
useCallback that calls submitConversation), replace the hardcoded "127.0.0.1" ip
with a real client IP lookup: fetch the client IP from https://api.ipify.org (or
use the same fallback logic used in
app/(default)/practice/_hooks/useCallSubmission.ts), handle fetch errors and
timeouts, then pass that resolved IP into submitConversation({ id: situationId,
script: scriptToSend, ip: resolvedIp }). Keep submitConversation and
conversationRef usage unchanged; only add the IP resolution step before the
call.
- Around line 318-329: The effect in useCallPage.ts that calls
startAudioOnUserClick should be guarded so it doesn't run when detailId is not
yet resolved; update the useEffect (the effect that currently depends on
[detailId]) to first check that detailId is truthy (and/or that
DEFAULT_CALL_SCRIPT has a valid audio URL) before scheduling the setTimeout and
calling startAudioOnUserClick, and skip/return early when detailId is empty to
avoid attempting to play DEFAULT_CALL_SCRIPT audio.
- Around line 160-268: The message handlers and postMessage calls in
initAudioIframe / playAudio (handlers named onMessage inside initAudioIframe and
playAudio, refs iframeRef, initPromiseRef, playTokenRef) and the cleanup
useEffect must be constrained to same-origin: change all postMessage targets to
use window.location.origin instead of "*" and in every MessageEvent handler add
an origin check (event.origin === window.location.origin) alongside the existing
event.source === cw check (and keep the react-devtools filter); ensure the
handler that resolves the initPromise and the playAudio handler both validate
origin before processing, and update the cleanup useEffect postMessage to send
to window.location.origin as well.

In
`@app/`(default)/practice/[situationId]/[detailId]/dial/call/hooks/useVoiceDetection.ts:
- Around line 99-103: The effect is calling cleanup() which synchronously calls
setAnalyserNode(null) and triggers a state update inside the same effect; split
the responsibilities so you do resource teardown synchronously but defer or move
the state update into a separate effect that runs when isRecording changes to
false (or on unmount). Specifically, keep cleanup() to release audio nodes and
stop streams (referenced by cleanup()), but remove the direct
setAnalyserNode(null) call from that function and instead add a small effect
like useEffect(() => { if (!isRecording) setAnalyserNode(null); },
[isRecording]) or otherwise defer the state reset (e.g., via
setTimeout/Promise.resolve) so setAnalyserNode is not invoked synchronously
inside the original effect.

In `@package.json`:
- Around line 24-25: The package.json dependency "speak-tts" is unmaintained and
should be addressed: either replace "speak-tts" with a maintained TTS
alternative (e.g., Web Speech API wrappers or a current npm package) or
explicitly document the risk and justification for keeping it; update
package.json to remove/replace the "speak-tts" entry or add a comment in your
repo docs describing the security/maintenance risk, include a brief migration
plan in the CHANGELOG or README, and update any code referencing "speak-tts"
(search for imports/usages of speak-tts) to use the chosen alternative or a
compatibility shim.

In `@scripts/generate-audio-files-simple.js`:
- Around line 23-26: The loop over sections (for (let i = 1; i <
sections.length; i += 2)) can access an undefined sectionContent when a trailing
header has no body; update the loop to check that sections[i + 1] exists before
using it (sections[i + 1] -> sectionContent) and skip or use an empty string
when undefined so subsequent calls like sectionContent.split("\n") won't throw;
modify the block that sets situationName and sectionContent to guard/return
early for undefined sectionContent (or normalize it to ""), ensuring variables
situationName and sectionContent are safe to use.
- Around line 79-83: The code currently interpolates cleaned text into shell
template strings when calling the macOS "say" command and later "ffmpeg",
causing a command injection risk; change both to use child_process.execFileSync
(or spawnSync) with the executable name ("say" and "ffmpeg") and an array of
arguments instead of a single interpolated string, pass the output path
(tempFile) and the cleaned text (from cleanText) as separate arguments, and
ensure you do not reintroduce shell parsing (do not use { shell: true }); update
the calls that currently use execSync and the ffmpeg template invocation to use
execFileSync/spawnSync with explicit args arrays so shell metacharacters in text
cannot be executed.

In `@scripts/generate-audio-files.js`:
- Around line 171-176: The Content-Length header is using data.length which
counts UTF-16 code units and will be wrong for non-ASCII payloads; update the
code that builds the options/headers (the options object where "Content-Length"
is set) to compute the byte length of the UTF-8 request body (the data variable)
using Buffer.byteLength(data, 'utf8') and use that value for "Content-Length"
before calling req.write(data).

In `@scripts/generate-audio-from-callscript.js`:
- Around line 192-196: The Content-Length header uses data.length which counts
UTF-16 code units and is wrong for non-ASCII (e.g. Korean); in the HTTPS request
options where the headers object is built (the "Content-Length" entry
referencing data), replace that calculation with the UTF-8 byte length (use
Buffer.byteLength(data, 'utf8')) so the Content-Length reflects actual bytes
sent when JSON-stringifying the Korean text.
- Around line 17-19: Replace the CommonJS requires for fs, path, execSync (const
fs, const path, const { execSync }) and the other require at line 171 with ESM
imports (e.g., import fs from "fs"; import path from "path"; import { execSync }
from "child_process"; and the corresponding import for the module used at line
171), or if you must keep CommonJS, wrap only the require statements with a
local ESLint disable/enable pair (/* eslint-disable
`@typescript-eslint/no-require-imports` */ before the require lines and /*
eslint-enable `@typescript-eslint/no-require-imports` */ after) so CI stops
flagging `@typescript-eslint/no-require-imports` violations; update all
occurrences (fs, path, execSync and the module used at line 171) consistently.

In `@scripts/GOOGLE_TTS_TROUBLESHOOTING.md`:
- Around line 72-77: The fenced code block containing the Korean TTS log snippet
(the block starting with ``` and lines like "📝 start.mp3" and "일일구입니다. 어떤 일이
발생했나요?") lacks a language specifier; update that opening fence to include a
language identifier (e.g., "text") so markdown renderers and accessibility tools
apply correct formatting and highlighting.
🧹 Nitpick comments (17)
app/(report)/report/[reportId]/components/ReportContent.tsx (1)

111-117: Next.js Image component usage looks correct.

The width and height props are required for static images in Next.js and are properly provided. The alt attribute is correctly set for accessibility.

One minor observation: the max-w-[140px] in className is effectively redundant since width={140} already sets the intrinsic width to 140px. Consider simplifying to just use the Image component's props for sizing.

Optional: Remove redundant max-width class
 				<Image
 					src={imagePath}
 					alt={result.title}
 					width={140}
 					height={140}
-					className="max-w-[140px] h-auto"
+					className="h-auto"
 				/>
app/(default)/practice/[situationId]/[detailId]/components/PracticeQuestionContent.tsx (1)

12-15: TODO placeholder with console.log should be addressed.

The handleOptionClick function only logs to console and has a TODO comment. If this is intentional scaffolding for a future implementation, consider removing the console.log before merging to avoid noise in production.

Would you like me to help implement the option click handling logic or open an issue to track this task?

app/(default)/practice/_hooks/useCallTimer.ts (1)

5-26: Consider memoizing formatTime or moving it outside the hook.

The formatTime function is recreated on every render. Since it's a pure function with no dependencies on component state other than its parameter, you could:

  1. Move it outside the hook as a module-level utility, or
  2. Wrap it with useCallback if it needs to be passed as a dependency.

Additionally, consider whether consumers might need timer controls (start/pause/reset) in the future.

♻️ Suggested improvement
+"use client";
+
+import { useEffect, useState } from "react";
+
+const formatTime = (totalSeconds: number) => {
+	const mins = Math.floor(totalSeconds / 60);
+	const secs = totalSeconds % 60;
+	return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
+};
+
 export function useCallTimer() {
 	const [seconds, setSeconds] = useState(0);
 
 	useEffect(() => {
 		const timer = setInterval(() => {
 			setSeconds((prev) => prev + 1);
 		}, 1000);
 
 		return () => clearInterval(timer);
 	}, []);
 
-	const formatTime = (totalSeconds: number) => {
-		const mins = Math.floor(totalSeconds / 60);
-		const secs = totalSeconds % 60;
-		return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
-	};
-
 	return {
 		seconds,
 		formattedTime: formatTime(seconds),
 	};
 }
public/audio-player.html (2)

14-15: Consider validating the message origin for defense-in-depth.

The message handler doesn't validate event.origin, meaning any parent window could potentially control this audio player. While this iframe is served from the same origin as your Next.js app, adding origin validation is a security best practice.

🔒 Suggested fix
 		// 부모 창으로부터 메시지 수신
 		window.addEventListener('message', async (event) => {
+			// Validate origin - adjust to your production domain
+			const allowedOrigins = [window.location.origin];
+			if (!allowedOrigins.includes(event.origin)) {
+				console.warn('[AudioPlayer] Ignored message from unknown origin:', event.origin);
+				return;
+			}
+
 			const { type, audioUrl } = event.data;

29-29: Use specific targetOrigin instead of '*' in postMessage calls.

Using '*' as the targetOrigin broadcasts messages to any listening window. For improved security, specify the expected parent origin.

🔒 Suggested approach

Store the validated origin from the first message and use it for all responses:

 		let currentAudio = null;
+		let parentOrigin = null;
 
 		window.addEventListener('message', async (event) => {
+			// Store and validate origin
+			if (!parentOrigin) {
+				parentOrigin = event.origin;
+			} else if (event.origin !== parentOrigin) {
+				return;
+			}
+
 			// ... rest of handler
 
-			parent.postMessage({ type: 'error', message: '...' }, '*');
+			parent.postMessage({ type: 'error', message: '...' }, parentOrigin);

Apply this pattern to all parent.postMessage() calls (lines 29, 42, 55, 69, 88, 102, 108).

app/(default)/practice/_components/PracticeQuestionContent.tsx (1)

12-15: TODO placeholder for option click handling.

The handleOptionClick function only logs to console. This is acceptable for scaffolding, but ensure this is tracked for implementation before release.

Would you like me to open an issue to track implementing the option selection logic, including navigation and state management?

app/(default)/practice/_hooks/useCallSubmission.ts (2)

30-56: Potential race condition with submission guard.

The isSubmitting check relies on state, which updates asynchronously. Rapid double-clicks could slip through before the state update propagates and the callback is recreated. Consider using a ref for immediate synchronous checking.

Proposed fix using ref for immediate guard
 export function useCallSubmission({ detailId }: UseCallSubmissionOptions) {
 	const [isSubmitting, setIsSubmitting] = useState(false);
 	const [reportId, setReportId] = useState<number | null>(null);
 	const [showEndPopup, setShowEndPopup] = useState(false);
+	const isSubmittingRef = useRef(false);

 	// ...

 	const handleSubmitConversation = useCallback(
 		async (script: ConversationScript[]) => {
-			if (isSubmitting) return;
+			if (isSubmittingRef.current) return;
+			isSubmittingRef.current = true;

 			setIsSubmitting(true);
 			try {
 				// ... existing logic
 			} catch (error) {
 				console.error("[API] 오류:", error);
 				alert("결과를 가져오는 중 오류가 발생했습니다.");
 			} finally {
 				setIsSubmitting(false);
+				isSubmittingRef.current = false;
 			}
 		},
-		[detailId, isSubmitting, getClientIP],
+		[detailId, getClientIP],
 	);

48-50: Consider replacing alert() with a more user-friendly notification.

Using alert() blocks the UI thread and provides a poor user experience. Consider using a toast notification system or updating component state to display an inline error message.

app/(report)/report/[reportId]/components/ReportErrorBoundary.tsx (1)

21-23: Minor: Redundant conditional check.

The error parameter is required per the interface, so the {error && ...} check on line 21 is always true. This can be simplified.

Proposed simplification
-				{error && (
-					<div className="text-caption text-gray-400">{error.message}</div>
-				)}
+				{error.message && (
+					<div className="text-caption text-gray-400">{error.message}</div>
+				)}
app/(default)/practice/_hooks/useVoiceDetection.ts (3)

37-80: Duplicated cleanup logic between cleanup() callback and useEffect cleanup.

The cleanup logic in the cleanup function (lines 37-80) and the useEffect return (lines 228-273) is substantially duplicated. This creates maintenance burden and risk of the two diverging. Consider extracting a shared cleanup helper or having the useEffect cleanup call the cleanup callback.

Proposed consolidation
 	return () => {
 		console.log("[Cleanup] useEffect cleanup 시작");
-		isInitializingRef.current = false;
-
-		// 타이머 정리
-		if (silenceTimerRef.current) {
-			clearTimeout(silenceTimerRef.current);
-			silenceTimerRef.current = null;
-		}
-
-		// 스트림 정리
-		if (streamRef.current) {
-			streamRef.current.getTracks().forEach((track) => {
-				track.stop();
-			});
-			streamRef.current = null;
-		}
-
-		// detect 정리 ...
-		// (all the duplicated code)
-
-		console.log("[Cleanup] useEffect cleanup 완료");
+		cleanup();
 	};

Also applies to: 228-273


219-223: Consider replacing alert() with a non-blocking notification.

Using alert() for the microphone permission error blocks the UI. Consider surfacing this error through returned state that the component can render as an inline message or modal.


148-159: Consider removing or conditionalizing verbose debug logging.

The frame-by-frame logging (first 10 frames) is useful for debugging but adds noise in production. Consider gating these logs behind a debug flag or process.env.NODE_ENV === 'development' check.

app/(default)/practice/[situationId]/[detailId]/dial/call/components/Waveform.tsx (1)

55-56: Guard check relies on closure state—ensure consistency.

The drawWaveform function checks isActive and analyserNode but these are captured from the effect closure. Since requestAnimationFrame continues calling drawWaveform, if isActive becomes false, the function correctly returns early. However, the canvasRef.current check was removed—if the canvas ref becomes null mid-animation (e.g., unmount race), ctx operations could fail.

Consider adding a guard for the canvas context validity inside the animation loop for robustness:

 const drawWaveform = () => {
-  if (!isActive || !analyserNode) return;
+  if (!isActive || !analyserNode || !ctx) return;
scripts/README.md (1)

76-85: Add language specifier to the fenced code block.

The directory structure code block is missing a language specifier. While this is a minor issue, it helps with consistent rendering and satisfies linter rules.

📝 Suggested fix
-```
+```text
 public/audio/
 ├── fire-far/
 │   ├── start.mp3
app/(default)/practice/[situationId]/[detailId]/dial/call/hooks/useVoiceDetection.ts (3)

54-56: Empty catch blocks silently swallow errors.

Several catch blocks (lines 54-56, 202-205, 289-291) silently ignore errors with just // ignore. While this may be intentional for non-critical operations, consider at minimum logging in development mode to aid debugging:

-} catch {
-  // ignore
-}
+} catch (e) {
+  if (process.env.NODE_ENV === 'development') {
+    console.debug('[SpeechRecognition] stop failed:', e);
+  }
+}

Also applies to: 289-291


207-211: Extract magic number for fallback timeout.

The 400ms fallback timeout should be documented or extracted as a named constant for clarity:

+const SPEECH_END_FALLBACK_TIMEOUT_MS = 400; // Fallback if onend doesn't fire
+
 // ...
 
 setTimeout(() => {
   if (pendingFinalizeRef.current) {
     finalize();
   }
-}, 400);
+}, SPEECH_END_FALLBACK_TIMEOUT_MS);

275-278: Consider a more user-friendly error notification than alert().

Using alert() blocks the main thread and provides a poor UX. Consider using a toast notification, modal, or callback to notify the parent component:

 } catch (error) {
   console.error("[Voice Detection] 초기화 오류:", error);
-  alert("마이크 접근 권한이 필요합니다.");
+  // Consider: onError?.(error) callback or toast notification
 }

Comment on lines +14 to +23
const [currentIndex, setCurrentIndex] = useState(0);
const onSelectRef = useRef(onSelect);
const isInitialMount = useRef(true);

// onSelect의 최신 값을 ref에 저장
useEffect(() => {
onSelectRef.current = onSelect;
}, [onSelect]);

const currentItem = items?.[currentIndex];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Index may become stale when items array changes.

If the items array shrinks (e.g., from 4 items to 2), currentIndex could remain at an invalid position, causing currentItem to be undefined and potential runtime errors in consumers.

🛡️ Suggested fix to reset index when items change
+	// Reset index if it becomes out of bounds when items change
+	useEffect(() => {
+		if (currentIndex >= items.length && items.length > 0) {
+			setCurrentIndex(0);
+		}
+	}, [items.length, currentIndex]);
+
 	const currentItem = items?.[currentIndex];
🤖 Prompt for AI Agents
In `@app/`(default)/_hooks/useCarousel.ts around lines 14 - 23, The currentIndex
state in the useCarousel hook can become invalid when the items array shrinks;
add a useEffect that watches items (and optionally items.length) and
resets/clamps currentIndex to a valid value (e.g., set to 0 if items is empty or
set to Math.min(currentIndex, items.length - 1)) so currentItem
(items[currentIndex]) never becomes undefined; update setCurrentIndex inside
that effect and reference the hook's currentIndex/items identifiers to locate
where to add this fix.

Comment on lines +26 to +66
useEffect(() => {
// 초기 마운트 시에는 실행하지 않음
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}

if (currentItem && onSelectRef.current) {
onSelectRef.current(currentItem);
}
}, [currentItem]);

const handlePrevious = () => {
setCurrentIndex((prev) => {
const newIndex = prev === 0 ? items.length - 1 : prev - 1;
const newItem = items[newIndex];
if (newItem) {
onSelect?.(newItem);
}
return newIndex;
});
};

const handleNext = () => {
setCurrentIndex((prev) => {
const newIndex = prev === items.length - 1 ? 0 : prev + 1;
const newItem = items[newIndex];
if (newItem) {
onSelect?.(newItem);
}
return newIndex;
});
};

const handleDotClick = (index: number) => {
setCurrentIndex(index);
const item = items[index];
if (item) {
onSelect?.(item);
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

onSelect is called twice per navigation action.

The handlers (handlePrevious, handleNext, handleDotClick) directly call onSelect, and then the useEffect on lines 26-36 also fires when currentItem changes, calling onSelectRef.current again. This causes the callback to execute twice.

Since currentIndex is internal state only modified by these handlers, the useEffect is redundant.

🐛 Remove redundant useEffect or direct calls

Option 1: Remove the useEffect (recommended - handlers already call onSelect)

-	// currentIndex가 변경될 때만 상위로 전달 (초기 마운트 제외)
-	useEffect(() => {
-		// 초기 마운트 시에는 실행하지 않음
-		if (isInitialMount.current) {
-			isInitialMount.current = false;
-			return;
-		}
-
-		if (currentItem && onSelectRef.current) {
-			onSelectRef.current(currentItem);
-		}
-	}, [currentItem]);

Option 2: Keep useEffect and remove direct calls in handlers

 	const handlePrevious = () => {
 		setCurrentIndex((prev) => {
 			const newIndex = prev === 0 ? items.length - 1 : prev - 1;
-			const newItem = items[newIndex];
-			if (newItem) {
-				onSelect?.(newItem);
-			}
 			return newIndex;
 		});
 	};
🤖 Prompt for AI Agents
In `@app/`(default)/_hooks/useCarousel.ts around lines 26 - 66, The useEffect
block watching currentItem (which checks isInitialMount and calls
onSelectRef.current) is redundant because the handlers handlePrevious,
handleNext, and handleDotClick already call onSelect directly, causing duplicate
calls; remove that useEffect (the block referencing isInitialMount.current,
currentItem, and onSelectRef.current) so onSelect is invoked only once from the
handlers, leaving handlePrevious, handleNext, handleDotClick, and the onSelect
prop as the single source of truth for selection callbacks.

Comment on lines +28 to +35
// 랜덤으로 하나 선택
const randomIndex = Math.floor(Math.random() * allDetailSituations.length);
return allDetailSituations[randomIndex];
};

const navigateToRandomPractice = () => {
const selected = getRandomSituation();
router.push(`/practice/${selected.situationId}/${selected.detailId}/dial`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a guard for empty array access.

If allDetailSituations is empty (e.g., data changes or import issues), randomIndex will be 0 and allDetailSituations[0] returns undefined, causing a runtime error when accessing selected.situationId.

🛡️ Suggested defensive check
 	const navigateToRandomPractice = () => {
 		const selected = getRandomSituation();
+		if (!selected) return;
 		router.push(`/practice/${selected.situationId}/${selected.detailId}/dial`);
 	};
🤖 Prompt for AI Agents
In `@app/`(default)/_hooks/useRandomPractice.ts around lines 28 - 35,
getRandomSituation currently assumes allDetailSituations has items and may
return undefined; update getRandomSituation (and its caller
navigateToRandomPractice) to guard against an empty array by checking
allDetailSituations.length === 0 and handling that case (e.g., return
null/undefined or throw a controlled error), then in navigateToRandomPractice
check the returned value before accessing selected.situationId/selected.detailId
and bail out or show an error/notification if no selection exists; modify
functions named getRandomSituation and navigateToRandomPractice accordingly so
they safely handle empty allDetailSituations.

Comment on lines +47 to +58
setHasProcessedAnswer(true);

// 대화 스크립트에 추가
conversationScriptRef.current = [
...conversationScriptRef.current,
newScript,
];

// 다음 질문으로 이동 또는 완료
if (!isLastQuestion) {
const nextIndex = currentQuestionIndex + 1;
setCurrentQuestionIndex(nextIndex);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

hasProcessedAnswer is not automatically reset between questions.

After setHasProcessedAnswer(true) on line 47, the flag remains true. When setCurrentQuestionIndex(nextIndex) is called on line 58, there's no automatic reset of hasProcessedAnswer. The caller must explicitly call resetAnswerProcessed() before the next answer can be processed.

If the reset is expected to happen automatically when the question changes, add it to the effect or within handleAnswerComplete after advancing the index.

Option: Auto-reset when question index changes
 	// 질문 인덱스가 0으로 리셋될 때 대화 스크립트도 초기화
 	useEffect(() => {
-		if (currentQuestionIndex === 0) {
-			conversationScriptRef.current = [];
-		}
+		// Reset script on index 0, and always reset processing flag on index change
+		if (currentQuestionIndex === 0) {
+			conversationScriptRef.current = [];
+		}
+		setHasProcessedAnswer(false);
 	}, [currentQuestionIndex]);
🤖 Prompt for AI Agents
In `@app/`(default)/practice/_hooks/useCallConversation.ts around lines 47 - 58,
The flag set by setHasProcessedAnswer(true) in handleAnswerComplete is not reset
when advancing questions, so hasProcessedAnswer stays true and blocks processing
the next answer; update handleAnswerComplete (the block that calls
setCurrentQuestionIndex(nextIndex)) to call resetAnswerProcessed() immediately
after advancing the index (or add an effect that watches currentQuestionIndex
and calls resetAnswerProcessed()), ensuring hasProcessedAnswer is cleared before
the next question is presented.

Comment on lines +18 to +27
const getClientIP = useCallback(async (): Promise<string> => {
try {
const response = await fetch("https://api.ipify.org?format=json");
const data = await response.json();
return data.ip || "192.168.1.1";
} catch (error) {
console.error("[IP] IP 조회 실패:", error);
return "192.168.1.1"; // 기본값
}
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Privacy consideration: External IP fetching.

Fetching the user's IP address from ipify.org raises privacy concerns and may require disclosure in your privacy policy. Additionally, this external dependency could fail or be blocked by corporate firewalls/ad blockers, affecting functionality.

Consider whether the backend could determine the client IP from the request headers instead, avoiding the need for client-side IP fetching entirely.

🤖 Prompt for AI Agents
In `@app/`(default)/practice/_hooks/useCallSubmission.ts around lines 18 - 27, The
client-side getClientIP function in useCallSubmission.ts fetches the IP from
ipify.org which raises privacy and reliability concerns; instead remove the
external fetch and obtain the client IP server-side (e.g., read request headers
on the backend endpoint that handles submissions) and then pass that IP into the
useCallSubmission flow or its submission API payload; update getClientIP to
either be removed or to call your own server endpoint that returns the IP
determined from the request (with fallback behavior removed or documented), and
ensure functions interacting with getClientIP (e.g., submitCall or whatever
handler in useCallSubmission) accept and propagate the server-derived IP rather
than calling ipify directly.

Comment on lines +79 to +83
try {
// say 명령어 실행 (한국어 음성: Yuna 사용)
execSync(`say -v Yuna -o "${tempFile}" "${cleanText(text)}"`, {
stdio: "pipe",
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Command injection vulnerability in shell execution.

The text parameter is passed directly to the shell command via template literal interpolation. Even though cleanText() removes HTML tags, it doesn't sanitize shell metacharacters. A malicious or malformed guide entry containing characters like $(...), backticks, or semicolons could execute arbitrary commands.

🔒 Proposed fix using proper argument escaping
 function generateWithSay(text, outputPath) {
 	const dir = path.dirname(outputPath);
 	if (!fs.existsSync(dir)) {
 		fs.mkdirSync(dir, { recursive: true });
 	}

 	console.log(`[say] 생성 중: ${path.basename(outputPath)}`);
 	console.log(
 		`  텍스트: ${text.substring(0, 50)}${text.length > 50 ? "..." : ""}`,
 	);

 	const tempFile = outputPath.replace(".mp3", ".aiff");
+	const cleanedText = cleanText(text);

 	try {
-		// say 명령어 실행 (한국어 음성: Yuna 사용)
-		execSync(`say -v Yuna -o "${tempFile}" "${cleanText(text)}"`, {
-			stdio: "pipe",
-		});
+		// say 명령어 실행 (한국어 음성: Yuna 사용) - use spawnSync to avoid shell injection
+		const { execFileSync } = require("child_process");
+		execFileSync("say", ["-v", "Yuna", "-o", tempFile, cleanedText], {
+			stdio: "pipe",
+		});

Similarly, the ffmpeg call on lines 87-90 should use execFileSync with array arguments instead of template strings.

🤖 Prompt for AI Agents
In `@scripts/generate-audio-files-simple.js` around lines 79 - 83, The code
currently interpolates cleaned text into shell template strings when calling the
macOS "say" command and later "ffmpeg", causing a command injection risk; change
both to use child_process.execFileSync (or spawnSync) with the executable name
("say" and "ffmpeg") and an array of arguments instead of a single interpolated
string, pass the output path (tempFile) and the cleaned text (from cleanText) as
separate arguments, and ensure you do not reintroduce shell parsing (do not use
{ shell: true }); update the calls that currently use execSync and the ffmpeg
template invocation to use execFileSync/spawnSync with explicit args arrays so
shell metacharacters in text cannot be executed.

Comment on lines +171 to +176
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": data.length,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd scripts && wc -l generate-audio-files.js

Repository: Team-NumberOne/119-web-client

Length of output: 100


🏁 Script executed:

head -180 scripts/generate-audio-files.js | tail -30

Repository: Team-NumberOne/119-web-client

Length of output: 749


🏁 Script executed:

sed -n '150,190p' scripts/generate-audio-files.js

Repository: Team-NumberOne/119-web-client

Length of output: 1090


🏁 Script executed:

sed -n '170,200p' scripts/generate-audio-files.js

Repository: Team-NumberOne/119-web-client

Length of output: 824


🏁 Script executed:

sed -n '190,220p' scripts/generate-audio-files.js

Repository: Team-NumberOne/119-web-client

Length of output: 803


Use byte length for Content-Length header.

data.length counts UTF-16 code units, not bytes. Since the request body is sent as UTF-8 (via req.write(data)), the Content-Length header will be incorrect for any non-ASCII characters in the request payload, such as the Korean text in this endpoint.

Suggested fix
-				"Content-Length": data.length,
+				"Content-Length": Buffer.byteLength(data),
🤖 Prompt for AI Agents
In `@scripts/generate-audio-files.js` around lines 171 - 176, The Content-Length
header is using data.length which counts UTF-16 code units and will be wrong for
non-ASCII payloads; update the code that builds the options/headers (the options
object where "Content-Length" is set) to compute the byte length of the UTF-8
request body (the data variable) using Buffer.byteLength(data, 'utf8') and use
that value for "Content-Length" before calling req.write(data).

Comment on lines +17 to +19
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the file exists and get basic info
if [ -f "scripts/generate-audio-from-callscript.js" ]; then
  echo "=== File found ==="
  wc -l scripts/generate-audio-from-callscript.js
else
  echo "=== File not found ==="
  find . -name "*generate-audio*" -type f 2>/dev/null | head -20
fi

Repository: Team-NumberOne/119-web-client

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Check for ESLint configuration files
echo "=== ESLint Config Files ==="
find . -maxdepth 2 -name ".eslintrc*" -o -name "eslint.config.*" 2>/dev/null | head -10

Repository: Team-NumberOne/119-web-client

Length of output: 120


🏁 Script executed:

#!/bin/bash
# Look at lines 17-19 and surrounding context, plus line 171
echo "=== Lines 15-25 of the script ==="
sed -n '15,25p' scripts/generate-audio-from-callscript.js 2>/dev/null
echo ""
echo "=== Line 171 context (169-173) ==="
sed -n '169,173p' scripts/generate-audio-from-callscript.js 2>/dev/null

Repository: Team-NumberOne/119-web-client

Length of output: 614


🏁 Script executed:

#!/bin/bash
# Read the ESLint config
echo "=== ESLint Configuration ==="
cat ./eslint.config.mjs

Repository: Team-NumberOne/119-web-client

Length of output: 551


🏁 Script executed:

#!/bin/bash
# Check package.json for module type
echo "=== Package.json type field ==="
grep -A 5 '"type"' package.json 2>/dev/null | head -20

Repository: Team-NumberOne/119-web-client

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Check if this is a Node script and what runtime it uses
echo "=== Shebang and first lines ==="
head -5 scripts/generate-audio-from-callscript.js

Repository: Team-NumberOne/119-web-client

Length of output: 173


🏁 Script executed:

#!/bin/bash
# Check if there are any ESLint rule overrides in the config or nearby files
echo "=== Check for eslintignore ==="
cat .eslintignore 2>/dev/null || echo "No .eslintignore found"

echo ""
echo "=== Check for rule config in eslint.config.mjs ==="
# Look for any rules or overrides
grep -i "no-require" . -r --include="*.mjs" --include="*.js" --include="*.json" 2>/dev/null | head -10

echo ""
echo "=== Look for TypeScript/Next config details ==="
node -e "console.log('Node version check')" 2>/dev/null && npm list eslint-config-next 2>/dev/null | head -5

Repository: Team-NumberOne/119-web-client

Length of output: 2957


🏁 Script executed:

#!/bin/bash
# Verify if `@typescript-eslint/no-require-imports` is part of next TypeScript config
echo "=== Search for TypeScript ESLint rules in the project ==="
find . -type f -name "*.mjs" -o -name ".eslintrc*" | xargs grep -l "typescript-eslint" 2>/dev/null | head -10

echo ""
echo "=== Check node_modules for the rule (search in eslint-config-next) ==="
if [ -d "node_modules/eslint-config-next" ]; then
  echo "Found eslint-config-next"
  find node_modules/eslint-config-next -name "*.js" -o -name "*.json" | xargs grep -l "no-require-imports" 2>/dev/null | head -5
else
  echo "eslint-config-next not found in node_modules"
fi

Repository: Team-NumberOne/119-web-client

Length of output: 435


🏁 Script executed:

#!/bin/bash
# Check if scripts directory is explicitly linted or ignored
echo "=== Search for scripts directory in eslint patterns ==="
find . -maxdepth 1 -name ".eslintignore" -o -name "package.json" | xargs grep -i "scripts" 2>/dev/null | head -20

echo ""
echo "=== Check Next.js and ESLint config for file inclusion patterns ==="
cat next.config.ts 2>/dev/null || cat next.config.js 2>/dev/null || echo "No next.config found"

echo ""
echo "=== Look for TypeScript parser configuration ==="
grep -r "parser" eslint.config.mjs

echo ""
echo "=== Check if eslintignore or gitignore excludes scripts ==="
cat .gitignore 2>/dev/null | grep -i script || echo "No script exclusion in gitignore"

Repository: Team-NumberOne/119-web-client

Length of output: 620


🏁 Script executed:

#!/bin/bash
# Determine whether TypeScript ESLint rules apply to .js files
echo "=== Check eslint-config-next TypeScript rules ==="
find node_modules/eslint-config-next -type f \( -name "typescript.js" -o -name "index.js" \) 2>/dev/null | xargs cat 2>/dev/null | head -100

echo ""
echo "=== Check if there are file-specific overrides ==="
grep -A 20 "files:" eslint.config.mjs 2>/dev/null || echo "No files pattern found"

Repository: Team-NumberOne/119-web-client

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Check the actual rules enabled in eslint-config-next/typescript
echo "=== Examine eslint-config-next typescript config ==="
find node_modules -path "*eslint-config-next*" -name "*.js" 2>/dev/null | grep -E "(typescript|index)" | head -3 | xargs cat 2>/dev/null | head -200

echo ""
echo "=== Check if TypeScript ESLint rules have file patterns ==="
grep -r "files.*\\.js\|files.*\\.ts" node_modules/eslint-config-next 2>/dev/null | head -10

Repository: Team-NumberOne/119-web-client

Length of output: 7750


🏁 Script executed:

#!/bin/bash
# Try to understand if the script file would actually be linted
echo "=== Check what files ESLint would process ==="
echo "Looking for ESLint file patterns in config..."
# Check if eslint-config-next specifies files
cat node_modules/eslint-config-next/index.js 2>/dev/null || echo "Not found"

echo ""
echo "=== Summary: Is scripts/ directory typically linted in Next.js projects? ==="
echo "scripts/ is typically a build/utility directory"
grep -i "scripts" package.json | head -5

Repository: Team-NumberOne/119-web-client

Length of output: 314


🏁 Script executed:

#!/bin/bash
# Directly check: does the file actually trigger linting errors?
echo "=== Checking actual lint status with ESLint ==="
if command -v npx &> /dev/null; then
  echo "Running ESLint on the target file..."
  npx eslint scripts/generate-audio-from-callscript.js 2>&1 | head -50 || echo "ESLint command executed"
else
  echo "npx not available in sandbox"
fi

Repository: Team-NumberOne/119-web-client

Length of output: 742


Fix @typescript-eslint/no-require-imports violations.

ESLint is reporting 4 errors for require() usage at lines 17–19 and 171. Either convert to ESM imports or disable the rule locally to prevent CI failures.

🛠️ Option: disable the rule locally
 #!/usr/bin/env node
+/* eslint-disable `@typescript-eslint/no-require-imports` */
 
 const fs = require("fs");
 const path = require("path");
 const { execSync } = require("child_process");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
#!/usr/bin/env node
/* eslint-disable `@typescript-eslint/no-require-imports` */
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
🧰 Tools
🪛 ESLint

[error] 17-17: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)


[error] 18-18: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)


[error] 19-19: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)

🤖 Prompt for AI Agents
In `@scripts/generate-audio-from-callscript.js` around lines 17 - 19, Replace the
CommonJS requires for fs, path, execSync (const fs, const path, const { execSync
}) and the other require at line 171 with ESM imports (e.g., import fs from
"fs"; import path from "path"; import { execSync } from "child_process"; and the
corresponding import for the module used at line 171), or if you must keep
CommonJS, wrap only the require statements with a local ESLint disable/enable
pair (/* eslint-disable `@typescript-eslint/no-require-imports` */ before the
require lines and /* eslint-enable `@typescript-eslint/no-require-imports` */
after) so CI stops flagging `@typescript-eslint/no-require-imports` violations;
update all occurrences (fs, path, execSync and the module used at line 171)
consistently.

Comment on lines +192 to +196
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": data.length,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l scripts/generate-audio-from-callscript.js

Repository: Team-NumberOne/119-web-client

Length of output: 118


🏁 Script executed:

sed -n '180,210p' scripts/generate-audio-from-callscript.js

Repository: Team-NumberOne/119-web-client

Length of output: 760


🏁 Script executed:

sed -n '1,195p' scripts/generate-audio-from-callscript.js | tail -100

Repository: Team-NumberOne/119-web-client

Length of output: 2574


🏁 Script executed:

sed -n '145,220p' scripts/generate-audio-from-callscript.js

Repository: Team-NumberOne/119-web-client

Length of output: 1999


Fix Content-Length calculation for Korean text.

data.length counts UTF-16 code units instead of bytes. When Korean text is JSON-stringified and sent with https.request(), the Content-Length header must reflect the UTF-8 byte count. Use Buffer.byteLength(data) instead.

Suggested fix
-					"Content-Length": data.length,
+					"Content-Length": Buffer.byteLength(data),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": data.length,
},
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(data),
},
🤖 Prompt for AI Agents
In `@scripts/generate-audio-from-callscript.js` around lines 192 - 196, The
Content-Length header uses data.length which counts UTF-16 code units and is
wrong for non-ASCII (e.g. Korean); in the HTTPS request options where the
headers object is built (the "Content-Length" entry referencing data), replace
that calculation with the UTF-8 byte length (use Buffer.byteLength(data,
'utf8')) so the Content-Length reflects actual bytes sent when JSON-stringifying
the Korean text.

Comment on lines +72 to +77
```
📝 start.mp3
"일일구입니다. 어떤 일이 발생했나요?"
⚠ 502 발생, 1000ms 후 재시도 (1/3)...
✓ MP3 생성 완료 (Google TTS) (재시도 2회)
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language specifier to fenced code block.

The code block is missing a language identifier, which affects syntax highlighting and accessibility tools.

📝 Suggested fix
-```
+```text
 📝 start.mp3
    "일일구입니다. 어떤 일이 발생했나요?"
    ⚠ 502 발생, 1000ms 후 재시도 (1/3)...
    ✓ MP3 생성 완료 (Google TTS) (재시도 2회)
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

72-72: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@scripts/GOOGLE_TTS_TROUBLESHOOTING.md` around lines 72 - 77, The fenced code
block containing the Korean TTS log snippet (the block starting with ``` and
lines like "📝 start.mp3" and "일일구입니다. 어떤 일이 발생했나요?") lacks a language
specifier; update that opening fence to include a language identifier (e.g.,
"text") so markdown renderers and accessibility tools apply correct formatting
and highlighting.

@choihooo choihooo merged commit af0c712 into main Jan 25, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant