diff --git a/sharedPrompts/my-app/package-lock.json b/sharedPrompts/my-app/package-lock.json index 9857369..6de4390 100644 --- a/sharedPrompts/my-app/package-lock.json +++ b/sharedPrompts/my-app/package-lock.json @@ -12,6 +12,7 @@ "@sentry/react": "^10.36.0", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", + "@tanstack/react-query": "^5.0.0", "@tosspayments/payment-sdk": "^1.9.2", "axios": "^1.13.2", "classnames": "^2.5.1", @@ -2323,6 +2324,32 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tosspayments/payment__types": { "version": "1.69.0", "resolved": "https://registry.npmjs.org/@tosspayments/payment__types/-/payment__types-1.69.0.tgz", diff --git a/sharedPrompts/my-app/src/features/prompt-builder/model/usePromptRecommendations.ts b/sharedPrompts/my-app/src/features/prompt-builder/model/usePromptRecommendations.ts index 85e47e7..bf913d8 100644 --- a/sharedPrompts/my-app/src/features/prompt-builder/model/usePromptRecommendations.ts +++ b/sharedPrompts/my-app/src/features/prompt-builder/model/usePromptRecommendations.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { useDebounce } from '@/shared/hooks/useDebounce'; import { recommendPrompts } from '../api/prompt-builder.api'; import type { PromptBuilderState, UserOverrides } from '../types/prompt-builder.types'; @@ -35,7 +35,7 @@ export function usePromptRecommendations() { const debouncedRawInput = useDebounce(state.rawInput, 500); - const { data: recommendations, isLoading, isError } = useQuery({ + const { data: recommendations, isLoading, isFetching, isError } = useQuery({ queryKey: ['promptRecommendations', debouncedRawInput, state.category], queryFn: () => recommendPrompts({ @@ -43,8 +43,9 @@ export function usePromptRecommendations() { category: state.category || undefined, raw_input: debouncedRawInput, }), - enabled: debouncedRawInput.length > 2, + enabled: debouncedRawInput.trim().length > 0, staleTime: 1000 * 60 * 5, + placeholderData: keepPreviousData, }); // Apply recommendations automatically for fields not overridden by the user @@ -58,8 +59,10 @@ export function usePromptRecommendations() { field: keyof PromptBuilderState['selectedAxes'], recommendedValue?: string ) => { - if (!overrides[field] && recommendedValue && prev.selectedAxes[field] !== recommendedValue) { - nextState.selectedAxes[field] = recommendedValue; + // 백엔드가 추천해주지 않았으면 빈 문자열로 리셋 (사용자가 수동으로 선택하지 않은 경우) + const newVal = recommendedValue || ''; + if (!overrides[field] && prev.selectedAxes[field] !== newVal) { + nextState.selectedAxes[field] = newVal; hasChanges = true; } }; @@ -76,17 +79,15 @@ export function usePromptRecommendations() { return hasChanges ? nextState : prev; }); - if (recommendations.axis_sources) { - setAxisSources(prev => { - const userSelected = Object.fromEntries( - Object.entries(prev).filter(([_, val]) => val === 'USER_SELECTED') - ); - return { - ...recommendations.axis_sources, - ...userSelected, - }; - }); - } + setAxisSources(prev => { + const userSelected = Object.fromEntries( + Object.entries(prev).filter(([_, val]) => val === 'USER_SELECTED') + ); + return { + ...(recommendations.axis_sources || {}), + ...userSelected, + }; + }); } }, [recommendations, overrides]); @@ -120,7 +121,7 @@ export function usePromptRecommendations() { overrides, axisSources, recommendations, - isLoading, + isLoading: isLoading || isFetching, isError, updateRawInput, updateCategory, diff --git a/sharedPrompts/my-app/src/features/prompt-builder/types/prompt-builder.types.ts b/sharedPrompts/my-app/src/features/prompt-builder/types/prompt-builder.types.ts index 01e5ca7..bd415ce 100644 --- a/sharedPrompts/my-app/src/features/prompt-builder/types/prompt-builder.types.ts +++ b/sharedPrompts/my-app/src/features/prompt-builder/types/prompt-builder.types.ts @@ -1,5 +1,6 @@ +/** POST /prompts/recommend — 백엔드 RecommendPromptRequest */ export interface RecommendPromptRequest { - request_mode: string; // e.g. 'SIMPLE' + request_mode: string; // RequestMode: SIMPLE | EXTRACTION | ADVANCED category?: string; intent?: string; role_type?: string; @@ -11,6 +12,7 @@ export interface RecommendPromptRequest { raw_input: string; } +/** POST /prompts/recommend 응답 — 백엔드 RecommendPromptResponse */ export interface RecommendPromptResponse { request_mode: string; category: string; @@ -29,6 +31,7 @@ export interface RecommendPromptResponse { default_selection?: string; } +/** POST /prompts/generate/confirmed — 백엔드 ConfirmedGeneratePromptRequest */ export interface ConfirmedGeneratePromptRequest { request_mode: string; category: string; @@ -39,24 +42,15 @@ export interface ConfirmedGeneratePromptRequest { style?: string; language?: string; experience?: string; - input: string; // maps from raw_input + input: string; json_schema?: string; title?: string; description?: string; tags?: string[]; } -export interface UnifiedGeneratePromptResponse { - output: string; - resolved_category?: string; - resolved_intent?: string; - semantic_resolution_summary?: string; - axis_sources?: Record; - recommendation_hints?: string[]; - validation_warnings?: string[]; - schema_failure_reasons?: string[]; - quality_badges?: Array<{ name: string; description: string }>; -} +/** 통합 생성 응답은 prompt.types.UnifiedGeneratePromptResponse 사용 */ +export type { UnifiedGeneratePromptResponse } from '@/features/prompt/types/prompt.types'; export interface PromptBuilderState { rawInput: string; diff --git a/sharedPrompts/my-app/src/features/prompt-builder/ui/PromptBuilderPage.tsx b/sharedPrompts/my-app/src/features/prompt-builder/ui/PromptBuilderPage.tsx index b2c1e89..8895c49 100644 --- a/sharedPrompts/my-app/src/features/prompt-builder/ui/PromptBuilderPage.tsx +++ b/sharedPrompts/my-app/src/features/prompt-builder/ui/PromptBuilderPage.tsx @@ -1,12 +1,25 @@ import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { PromptInputPanel } from './PromptInputPanel'; import { SemanticSettingsPanel } from './SemanticSettingsPanel'; import { PromptResultPanel } from './PromptResultPanel'; import { usePromptRecommendations } from '../model/usePromptRecommendations'; import { generateConfirmedPrompt } from '../api/prompt-builder.api'; import type { UnifiedGeneratePromptResponse } from '../types/prompt-builder.types'; +import { ArrowLeft } from 'lucide-react'; +import { + TONE_DISPLAY_NAMES, + EXPERIENCE_DISPLAY_NAMES, + STYLE_DISPLAY_NAMES, + LANGUAGE_DISPLAY_NAMES, +} from '@/features/prompt/model/shared/enumDisplayNames'; +import { ActionIntent, PromptCategory } from '@/features/prompt/types/prompt.types'; +import { getActionTypesForCategory, getRoleTypesForCategory } from '@/features/prompt/types/category-mapping.types'; + +import { CreatePromptSuccess } from '@/features/prompt/ui/create/CreatePromptSuccess'; export const PromptBuilderPage: React.FC = () => { + const navigate = useNavigate(); const { state, overrides, @@ -22,11 +35,29 @@ export const PromptBuilderPage: React.FC = () => { const [generationResult, setGenerationResult] = useState(null); const [isGenerating, setIsGenerating] = useState(false); const [generationError, setGenerationError] = useState(null); + const [saved, setSaved] = useState(false); + + const handleBack = () => { + navigate('/feed'); // or navigate(-1) + }; + + const fallbackIntents = Object.values(ActionIntent).filter(i => i !== 'DEBUG' && i !== 'DESIGN' && i !== 'CODE'); + const fallbackRoles = state.category ? (getRoleTypesForCategory(state.category as PromptCategory) || []) : []; + const fallbackActions = state.category ? (getActionTypesForCategory(state.category as PromptCategory) || []) : []; + + const validIntents = recommendations?.intent_candidates?.filter(Boolean) || []; + const validRoles = recommendations?.role_candidates?.filter(Boolean) || []; + const validActions = recommendations?.action_candidates?.filter(Boolean) || []; + + const resolvedIntents = validIntents.length > 0 ? validIntents : fallbackIntents; + const resolvedRoles = validRoles.length > 0 ? validRoles : fallbackRoles; + const resolvedActions = validActions.length > 0 ? validActions : fallbackActions; const handleGenerate = async () => { setIsGenerating(true); setGenerationError(null); setGenerationResult(null); + setSaved(false); try { const response = await generateConfirmedPrompt({ request_mode: 'SIMPLE', @@ -41,6 +72,7 @@ export const PromptBuilderPage: React.FC = () => { input: state.rawInput.trim(), }); setGenerationResult(response); + setSaved(true); } catch (error) { console.error('Failed to generate prompt', error); setGenerationError('Failed to generate prompt. Please try again.'); @@ -53,66 +85,91 @@ export const PromptBuilderPage: React.FC = () => { state.rawInput.trim().length > 0 && state.selectedAxes.intent.trim().length > 0; - return ( -
-
-

AI Prompt Builder

-

Tell us what you want to create, and we'll automatically suggest the best settings.

-
+ if (saved) { + return { + setSaved(false); + setGenerationResult(null); + }} />; + } -
- {/* Left Column: Input & Settings */} -
- - - {isRecommendError && ( -
+
+
+
+
- )} - - + + +

AI Prompt Builder

+

Tell us what you want to create, and we'll automatically suggest the best settings.

+
+ + +
+
+ {/* Left Column: Input & Settings */} +
+ + + {isRecommendError && ( +
+ 추천을 불러오는 데 실패했습니다. 수동으로 설정을 선택해주세요. +
+ )} - {/* Right Column: Preview & Output */} -
- {generationError && ( -
- {generationError} -
- )} - + +
+ + {/* Right Column: Preview & Output */} +
+ {generationError && ( +
+ {generationError} +
+ )} + +
-
+
); }; diff --git a/sharedPrompts/my-app/src/features/prompt-builder/ui/PromptInputPanel.tsx b/sharedPrompts/my-app/src/features/prompt-builder/ui/PromptInputPanel.tsx index 264a969..45b5f70 100644 --- a/sharedPrompts/my-app/src/features/prompt-builder/ui/PromptInputPanel.tsx +++ b/sharedPrompts/my-app/src/features/prompt-builder/ui/PromptInputPanel.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { PromptCategory, PROMPT_CATEGORY_DISPLAY_NAMES } from '@/features/prompt/types/prompt.types'; interface PromptInputPanelProps { rawInput: string; @@ -15,31 +16,32 @@ export const PromptInputPanel: React.FC = ({ }) => { return (
-

Prompt Builder

+

프롬프트 빌더

- +
- +