diff --git a/src/components/modals/ExerciseModal.tsx b/src/components/modals/ExerciseModal.tsx index 5185eb2..5ae5337 100644 --- a/src/components/modals/ExerciseModal.tsx +++ b/src/components/modals/ExerciseModal.tsx @@ -12,8 +12,8 @@ import { KeyboardAvoidingView, Platform, Alert, - SafeAreaView, } from "react-native"; +import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"; import { Ionicons as Icon } from "@expo/vector-icons"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { colors } from "../../theme/colors"; @@ -118,6 +118,7 @@ const ExerciseModal: React.FC = ({ renderContentOnly = false, isCompleted = false, }) => { + const insets = useSafeAreaInsets(); const [searchTerm, setSearchTerm] = useState(""); const [selectedCategory, setSelectedCategory] = useState("전체"); const [currentMode, setCurrentMode] = useState<"add" | "edit" | "detail">( @@ -1449,7 +1450,7 @@ const getExerciseDisplayName = React.useCallback( : []; const content = fullScreen ? ( - + {currentMode === "add" ? ( - {hasSequenceControls && ( - - handleSequenceNavigatePress("prev")} - disabled={!hasPrevSequence} - > - - 이전 운동 - - - {!isCompleted && ( - - 타이머 - - {formatWorkoutTimer(workoutTimerSeconds)} - - - )} - handleSequenceNavigatePress("next")} - disabled={!canNextExercise} - > - - 다음 운동 - - - - )} - - - 운동 끝내기 - - {allSetsCompleted && !isCompleted && (() => { const currentExerciseName = getExerciseDisplayName( selectedExercise || exerciseData || { name: "" } @@ -2305,6 +2236,77 @@ const getExerciseDisplayName = React.useCallback( ); })()} + {hasSequenceControls && ( + + handleSequenceNavigatePress("prev")} + disabled={!hasPrevSequence} + > + + 이전 운동 + + + {!isCompleted && ( + + 타이머 + + {formatWorkoutTimer(workoutTimerSeconds)} + + + )} + handleSequenceNavigatePress("next")} + disabled={!canNextExercise} + > + + 다음 운동 + + + + )} + + + 운동 끝내기 + + )} @@ -3219,9 +3221,9 @@ const styles = StyleSheet.create({ elevation: 3, }, feedbackSection: { - marginTop: 12, + marginTop: 0, marginHorizontal: 0, - marginBottom: 0, + marginBottom: 12, }, feedbackTitle: { fontSize: 18, @@ -3827,8 +3829,8 @@ const styles = StyleSheet.create({ }, footer: { paddingHorizontal: 20, - paddingBottom: 8, - paddingTop: 8, + paddingBottom: 0, + paddingTop: 16, backgroundColor: "#2a2a2a", }, footerExtended: { @@ -3836,7 +3838,7 @@ const styles = StyleSheet.create({ gap: 6, }, footerWithFeedback: { - paddingBottom: 16, + paddingBottom: 0, }, saveExerciseBtn: { backgroundColor: "#e3ff7c", diff --git a/src/screens/analysis/AnalysisScreen.tsx b/src/screens/analysis/AnalysisScreen.tsx index 18a2b60..76e73bb 100644 --- a/src/screens/analysis/AnalysisScreen.tsx +++ b/src/screens/analysis/AnalysisScreen.tsx @@ -1896,6 +1896,7 @@ const AnalysisScreen = ({ navigation }: any) => { // 랜덤 코멘트 로드 const loadRandomScoreComment = useCallback(async () => { try { + console.log("[ANALYSIS][COMMENT] 코멘트 로드 시작"); setScoreCommentLoading(true); setScoreComment(null); @@ -1907,21 +1908,35 @@ const AnalysisScreen = ({ navigation }: any) => { ]; const randomIndex = Math.floor(Math.random() * commentTypes.length); const selectedCommentAPI = commentTypes[randomIndex]; + const apiType = randomIndex === 0 ? "daily" : randomIndex === 1 ? "weekly" : "monthly"; - console.log("[ANALYSIS] 코멘트 API 선택:", randomIndex === 0 ? "daily" : randomIndex === 1 ? "weekly" : "monthly"); + console.log("[ANALYSIS][COMMENT] 코멘트 API 선택:", apiType, "index:", randomIndex); const comment = await selectedCommentAPI(); + console.log("[ANALYSIS][COMMENT] API 호출 완료, 반환된 comment:", comment); + console.log("[ANALYSIS][COMMENT] comment 타입:", typeof comment); + console.log("[ANALYSIS][COMMENT] comment가 truthy인가?", !!comment); + console.log("[ANALYSIS][COMMENT] comment가 문자열인가?", typeof comment === 'string'); + console.log("[ANALYSIS][COMMENT] comment 길이:", comment ? (typeof comment === 'string' ? comment.length : 'N/A') : 0); + if (comment) { setScoreComment(comment); - console.log("[ANALYSIS] 코멘트 로드 성공:", comment); + console.log("[ANALYSIS][COMMENT] 코멘트 로드 성공, state에 설정:", comment); } else { - console.log("[ANALYSIS] 코멘트 없음"); + console.log("[ANALYSIS][COMMENT] 코멘트 없음 (null 또는 빈 값)"); } - } catch (error) { - console.error("[ANALYSIS] 코멘트 로드 실패:", error); + } catch (error: any) { + console.error("[ANALYSIS][COMMENT] 코멘트 로드 실패:", { + message: error?.message, + status: error?.status, + data: error?.data, + stack: error?.stack, + error: error, + }); setScoreComment(null); } finally { setScoreCommentLoading(false); + console.log("[ANALYSIS][COMMENT] 코멘트 로드 완료, loading: false"); } }, []); diff --git a/src/screens/chatbot/ChatbotScreen.tsx b/src/screens/chatbot/ChatbotScreen.tsx index 175a94e..86b0fd8 100644 --- a/src/screens/chatbot/ChatbotScreen.tsx +++ b/src/screens/chatbot/ChatbotScreen.tsx @@ -561,7 +561,7 @@ const ChatbotScreen = ({ navigation }: any) => { 0 ? insets.bottom : 10 }, + { paddingBottom: insets.bottom > 0 ? Math.max(insets.bottom, 4) : 4 }, ]} > 0) { - const lines = instructions + // 운동 설명과 동일한 방식으로 Step 처리 + const stepMatches: Array<{ number: string; content: string }> = + []; + + // 먼저 줄바꿈으로 분리 + const instructionLines = instructions + .split("\n") + .filter((line) => line.trim()); + + instructionLines.forEach((line) => { + // 한 줄에 여러 Step이 쉼표로 구분되어 있을 수 있음 + // "Step: 1 ..., Step:2 ..., Step:3 ..." 형식 처리 + + // Step: 또는 ,Step: 패턴으로 분리 + const stepParts = line + .split(/(?:^|,\s*)Step:/i) + .filter((part) => part.trim()); + const foundSteps: Array<{ + stepNum: number; + description: string; + }> = []; + + stepParts.forEach((part) => { + // Step: 다음에 오는 숫자와 설명 추출 + const stepMatch = part.match(/^\s*(\d+)\s*(.+)$/); + if (stepMatch) { + const stepNum = parseInt(stepMatch[1], 10); + let description = stepMatch[2].trim(); + // 끝의 쉼표, 점, 공백 제거 + description = description.replace(/[,\.\s]+$/, "").trim(); + + if (description) { + foundSteps.push({ stepNum, description }); + } + } + }); + + // Step: 패턴으로 분리되지 않은 경우, 정규식으로 다시 시도 + if (foundSteps.length === 0) { + const stepRegex = /Step:\s*(\d+)\s*([^,]+?)(?=,\s*Step:|$)/gi; + let match; + + while ((match = stepRegex.exec(line)) !== null) { + const stepNum = parseInt(match[1], 10); + let description = match[2].trim(); + description = description.replace(/[,\.\s]+$/, "").trim(); + + if (description) { + foundSteps.push({ stepNum, description }); + } + } + } + + // 찾은 Step들을 stepMatches에 추가 + foundSteps.forEach(({ stepNum, description }) => { + stepMatches.push({ + number: stepNum.toString(), + content: description, + }); + }); + }); + + // Step 패턴이 있으면 Step 기준으로 렌더링 + if (stepMatches.length > 0) { + return stepMatches.map((step, idx) => { + return ( + + + + {step.number} + + + {step.content} + + + + ); + }); + } + + // Step 패턴이 없으면 기존대로 줄바꿈으로 분리 + const fallbackLines = instructions .split("\n") .filter((line) => line.trim()); - return lines.map((line: string, idx: number) => ( + return fallbackLines.map((line: string, idx: number) => ( {line.trim()} diff --git a/src/services/apiConfig.ts b/src/services/apiConfig.ts index 4fac13d..b75d69a 100644 --- a/src/services/apiConfig.ts +++ b/src/services/apiConfig.ts @@ -271,6 +271,11 @@ export const requestAI = async ( } console.log("✅ AI 서버 응답 성공"); + console.log("📦 AI 서버 응답 데이터:", JSON.stringify(data, null, 2)); + console.log("📦 AI 서버 응답 데이터 타입:", typeof data); + if (typeof data === 'object' && data !== null) { + console.log("📦 AI 서버 응답 데이터 키들:", Object.keys(data)); + } return data; } catch (error: any) { // 타임아웃 에러 diff --git a/src/services/healthScoreAPI.ts b/src/services/healthScoreAPI.ts index 779e59b..9aa3e4f 100644 --- a/src/services/healthScoreAPI.ts +++ b/src/services/healthScoreAPI.ts @@ -145,12 +145,49 @@ export const healthScoreAPI = { getDailyComment: async (): Promise => { try { const userId = await getUserId(); - const response = await requestAI<{ comment: string }>( + console.log("[HEALTH_SCORE][COMMENT] 일일 코멘트 요청 시작, userId:", userId); + const response = await requestAI( `/score/comment/daily/${userId}`, { method: "GET" } ); - return response?.comment || null; + console.log("[HEALTH_SCORE][COMMENT] 일일 코멘트 응답 전체:", JSON.stringify(response, null, 2)); + console.log("[HEALTH_SCORE][COMMENT] response 타입:", typeof response); + console.log("[HEALTH_SCORE][COMMENT] response가 문자열인가?", typeof response === 'string'); + console.log("[HEALTH_SCORE][COMMENT] response 키들:", response && typeof response === 'object' ? Object.keys(response) : 'N/A'); + console.log("[HEALTH_SCORE][COMMENT] response.comment:", response?.comment); + console.log("[HEALTH_SCORE][COMMENT] response.comments:", response?.comments); + console.log("[HEALTH_SCORE][COMMENT] response.message:", response?.message); + + // 응답이 문자열인 경우 + if (typeof response === 'string') { + console.log("[HEALTH_SCORE][COMMENT] 응답이 문자열로 받아짐, 반환:", response); + return response.trim() || null; + } + + // 응답이 객체인 경우 - comments (복수형) 필드를 우선 확인 + let comment = response?.comments || response?.comment || response?.message || null; + + // comments가 배열인 경우 랜덤으로 하나 선택 + if (Array.isArray(comment) && comment.length > 0) { + const randomIndex = Math.floor(Math.random() * comment.length); + const selectedComment = comment[randomIndex]; + console.log("[HEALTH_SCORE][COMMENT] 배열에서 랜덤 선택:", { + totalComments: comment.length, + selectedIndex: randomIndex, + selectedComment: selectedComment, + }); + comment = selectedComment; + } + + console.log("[HEALTH_SCORE][COMMENT] 추출된 comment:", comment); + return typeof comment === 'string' && comment.trim() ? comment : null; } catch (error: any) { + console.error("[HEALTH_SCORE][COMMENT] 일일 코멘트 에러:", { + message: error.message, + status: error.status, + data: error.data, + stack: error.stack, + }); if (error.status === 404) { return null; } @@ -163,12 +200,49 @@ export const healthScoreAPI = { getWeeklyComment: async (): Promise => { try { const userId = await getUserId(); - const response = await requestAI<{ comment: string }>( + console.log("[HEALTH_SCORE][COMMENT] 주간 코멘트 요청 시작, userId:", userId); + const response = await requestAI( `/score/comment/weekly/${userId}`, { method: "GET" } ); - return response?.comment || null; + console.log("[HEALTH_SCORE][COMMENT] 주간 코멘트 응답 전체:", JSON.stringify(response, null, 2)); + console.log("[HEALTH_SCORE][COMMENT] response 타입:", typeof response); + console.log("[HEALTH_SCORE][COMMENT] response가 문자열인가?", typeof response === 'string'); + console.log("[HEALTH_SCORE][COMMENT] response 키들:", response && typeof response === 'object' ? Object.keys(response) : 'N/A'); + console.log("[HEALTH_SCORE][COMMENT] response.comment:", response?.comment); + console.log("[HEALTH_SCORE][COMMENT] response.comments:", response?.comments); + console.log("[HEALTH_SCORE][COMMENT] response.message:", response?.message); + + // 응답이 문자열인 경우 + if (typeof response === 'string') { + console.log("[HEALTH_SCORE][COMMENT] 응답이 문자열로 받아짐, 반환:", response); + return response.trim() || null; + } + + // 응답이 객체인 경우 - comments (복수형) 필드를 우선 확인 + let comment = response?.comments || response?.comment || response?.message || null; + + // comments가 배열인 경우 랜덤으로 하나 선택 + if (Array.isArray(comment) && comment.length > 0) { + const randomIndex = Math.floor(Math.random() * comment.length); + const selectedComment = comment[randomIndex]; + console.log("[HEALTH_SCORE][COMMENT] 배열에서 랜덤 선택:", { + totalComments: comment.length, + selectedIndex: randomIndex, + selectedComment: selectedComment, + }); + comment = selectedComment; + } + + console.log("[HEALTH_SCORE][COMMENT] 추출된 comment:", comment); + return typeof comment === 'string' && comment.trim() ? comment : null; } catch (error: any) { + console.error("[HEALTH_SCORE][COMMENT] 주간 코멘트 에러:", { + message: error.message, + status: error.status, + data: error.data, + stack: error.stack, + }); if (error.status === 404) { return null; } @@ -181,12 +255,49 @@ export const healthScoreAPI = { getMonthlyComment: async (): Promise => { try { const userId = await getUserId(); - const response = await requestAI<{ comment: string }>( + console.log("[HEALTH_SCORE][COMMENT] 월간 코멘트 요청 시작, userId:", userId); + const response = await requestAI( `/score/comment/monthly/${userId}`, { method: "GET" } ); - return response?.comment || null; + console.log("[HEALTH_SCORE][COMMENT] 월간 코멘트 응답 전체:", JSON.stringify(response, null, 2)); + console.log("[HEALTH_SCORE][COMMENT] response 타입:", typeof response); + console.log("[HEALTH_SCORE][COMMENT] response가 문자열인가?", typeof response === 'string'); + console.log("[HEALTH_SCORE][COMMENT] response 키들:", response && typeof response === 'object' ? Object.keys(response) : 'N/A'); + console.log("[HEALTH_SCORE][COMMENT] response.comment:", response?.comment); + console.log("[HEALTH_SCORE][COMMENT] response.comments:", response?.comments); + console.log("[HEALTH_SCORE][COMMENT] response.message:", response?.message); + + // 응답이 문자열인 경우 + if (typeof response === 'string') { + console.log("[HEALTH_SCORE][COMMENT] 응답이 문자열로 받아짐, 반환:", response); + return response.trim() || null; + } + + // 응답이 객체인 경우 - comments (복수형) 필드를 우선 확인 + let comment = response?.comments || response?.comment || response?.message || null; + + // comments가 배열인 경우 랜덤으로 하나 선택 + if (Array.isArray(comment) && comment.length > 0) { + const randomIndex = Math.floor(Math.random() * comment.length); + const selectedComment = comment[randomIndex]; + console.log("[HEALTH_SCORE][COMMENT] 배열에서 랜덤 선택:", { + totalComments: comment.length, + selectedIndex: randomIndex, + selectedComment: selectedComment, + }); + comment = selectedComment; + } + + console.log("[HEALTH_SCORE][COMMENT] 추출된 comment:", comment); + return typeof comment === 'string' && comment.trim() ? comment : null; } catch (error: any) { + console.error("[HEALTH_SCORE][COMMENT] 월간 코멘트 에러:", { + message: error.message, + status: error.status, + data: error.data, + stack: error.stack, + }); if (error.status === 404) { return null; } diff --git a/src/utils/inbodyApi.ts b/src/utils/inbodyApi.ts index 5920b7e..cff61de 100644 --- a/src/utils/inbodyApi.ts +++ b/src/utils/inbodyApi.ts @@ -534,12 +534,36 @@ export const getInBodyComment = async ( }, }); + // 🔍 응답 전체 데이터 로깅 추가 + console.log("[INBODY][COMMENT] AI 코멘트 조회 응답 전체:", JSON.stringify(response.data, null, 2)); + console.log("[INBODY][COMMENT] response.data 타입:", typeof response.data); + console.log("[INBODY][COMMENT] response.data 키들:", response.data && typeof response.data === 'object' ? Object.keys(response.data) : 'N/A'); + console.log("[INBODY][COMMENT] response.data.comment:", response.data?.comment); + console.log("[INBODY][COMMENT] response.data.comments:", response.data?.comments); + console.log("[INBODY][COMMENT] response.data.message:", response.data?.message); + + // 🔧 comments (복수형) 필드도 확인 + let comment = response.data?.comments || response.data?.comment || response.data?.message || ""; + + // comments가 배열인 경우 랜덤으로 하나 선택 + if (Array.isArray(comment) && comment.length > 0) { + const randomIndex = Math.floor(Math.random() * comment.length); + const selectedComment = comment[randomIndex]; + console.log("[INBODY][COMMENT] 배열에서 랜덤 선택:", { + totalComments: comment.length, + selectedIndex: randomIndex, + selectedComment: selectedComment, + }); + comment = selectedComment; + } + + console.log("[INBODY][COMMENT] 추출된 comment:", comment); console.log("[INBODY][COMMENT] AI 코멘트 조회 응답:", { status: response.status, - comment: response.data?.comment, + comment: comment, }); - return response.data; + return { comment: typeof comment === 'string' ? comment : "" }; } catch (error: any) { if (axios.isAxiosError(error)) { const status = error.response?.status;