feat: Bypass API 화면 개선 및 SPA 새로고침 오류 수정 (#63)
- WebClient Bean 드롭다운 표시 텍스트를 description만 표시 - bypass_api_param에 example 컬럼 추가 (Swagger placeholder 사용자 설정) - 목록 기본 뷰를 리스트뷰(테이블)로 변경 - 도메인명 드롭다운 필터 추가 - WebViewController에 /bypass-config 경로 추가 (SPA 새로고침 404 해결) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
fed2d6455a
커밋
e6e58bfc25
@ -14,6 +14,7 @@ export interface BypassParamDto {
|
|||||||
paramIn: string; // PATH, QUERY, BODY
|
paramIn: string; // PATH, QUERY, BODY
|
||||||
required: boolean;
|
required: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
|
example: string; // Swagger @Parameter example 값
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export default function BypassStepBasic({
|
|||||||
<option value="">선택하세요</option>
|
<option value="">선택하세요</option>
|
||||||
{webclientBeans.map((bean) => (
|
{webclientBeans.map((bean) => (
|
||||||
<option key={bean.name} value={bean.name}>
|
<option key={bean.name} value={bean.name}>
|
||||||
{bean.name}{bean.description ? ` — ${bean.description}` : ''}
|
{bean.description || bean.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ function createEmptyParam(sortOrder: number): BypassParamDto {
|
|||||||
paramIn: 'QUERY',
|
paramIn: 'QUERY',
|
||||||
required: false,
|
required: false,
|
||||||
description: '',
|
description: '',
|
||||||
|
example: '',
|
||||||
sortOrder,
|
sortOrder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -58,6 +59,7 @@ export default function BypassStepParams({ params, onChange }: BypassStepParamsP
|
|||||||
<th className="pb-2 text-left font-medium text-wing-muted pr-3 min-w-[100px]">위치</th>
|
<th className="pb-2 text-left font-medium text-wing-muted pr-3 min-w-[100px]">위치</th>
|
||||||
<th className="pb-2 text-center font-medium text-wing-muted pr-3 w-14">필수</th>
|
<th className="pb-2 text-center font-medium text-wing-muted pr-3 w-14">필수</th>
|
||||||
<th className="pb-2 text-left font-medium text-wing-muted pr-3">설명</th>
|
<th className="pb-2 text-left font-medium text-wing-muted pr-3">설명</th>
|
||||||
|
<th className="pb-2 text-left font-medium text-wing-muted pr-3 min-w-[120px]">Example</th>
|
||||||
<th className="pb-2 w-10"></th>
|
<th className="pb-2 w-10"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -112,6 +114,15 @@ export default function BypassStepParams({ params, onChange }: BypassStepParamsP
|
|||||||
className="w-full px-2 py-1.5 text-sm rounded border border-wing-border bg-wing-surface text-wing-text placeholder:text-wing-muted focus:outline-none focus:ring-1 focus:ring-wing-accent/50"
|
className="w-full px-2 py-1.5 text-sm rounded border border-wing-border bg-wing-surface text-wing-text placeholder:text-wing-muted focus:outline-none focus:ring-1 focus:ring-wing-accent/50"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="py-2 pr-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={param.example}
|
||||||
|
onChange={(e) => handleChange(index, 'example', e.target.value)}
|
||||||
|
placeholder="예: 9876543"
|
||||||
|
className="w-full px-2 py-1.5 text-sm rounded border border-wing-border bg-wing-surface text-wing-text placeholder:text-wing-muted focus:outline-none focus:ring-1 focus:ring-wing-accent/50"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td className="py-2">
|
<td className="py-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -32,8 +32,9 @@ export default function BypassConfig() {
|
|||||||
const [configs, setConfigs] = useState<BypassConfigResponse[]>([]);
|
const [configs, setConfigs] = useState<BypassConfigResponse[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [webclientBeans, setWebclientBeans] = useState<WebClientBeanInfo[]>([]);
|
const [webclientBeans, setWebclientBeans] = useState<WebClientBeanInfo[]>([]);
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('card');
|
const [viewMode, setViewMode] = useState<ViewMode>('table');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedDomain, setSelectedDomain] = useState('');
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editConfig, setEditConfig] = useState<BypassConfigResponse | null>(null);
|
const [editConfig, setEditConfig] = useState<BypassConfigResponse | null>(null);
|
||||||
@ -110,15 +111,21 @@ export default function BypassConfig() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const domainNames = useMemo(() => {
|
||||||
|
const names = [...new Set(configs.map((c) => c.domainName))];
|
||||||
|
return names.sort();
|
||||||
|
}, [configs]);
|
||||||
|
|
||||||
const filteredConfigs = useMemo(() => {
|
const filteredConfigs = useMemo(() => {
|
||||||
if (!searchTerm.trim()) return configs;
|
return configs.filter((c) => {
|
||||||
const term = searchTerm.toLowerCase();
|
const matchesSearch =
|
||||||
return configs.filter(
|
!searchTerm.trim() ||
|
||||||
(c) =>
|
c.domainName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
c.domainName.toLowerCase().includes(term) ||
|
c.displayName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
c.displayName.toLowerCase().includes(term),
|
const matchesDomain = !selectedDomain || c.domainName === selectedDomain;
|
||||||
);
|
return matchesSearch && matchesDomain;
|
||||||
}, [configs, searchTerm]);
|
});
|
||||||
|
}, [configs, searchTerm, selectedDomain]);
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
|
|
||||||
@ -177,6 +184,18 @@ export default function BypassConfig() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 도메인 드롭다운 필터 */}
|
||||||
|
<select
|
||||||
|
value={selectedDomain}
|
||||||
|
onChange={(e) => setSelectedDomain(e.target.value)}
|
||||||
|
className="px-3 py-2 text-sm rounded-lg border border-wing-border bg-wing-surface text-wing-text"
|
||||||
|
>
|
||||||
|
<option value="">전체 도메인</option>
|
||||||
|
{domainNames.map((name) => (
|
||||||
|
<option key={name} value={name}>{name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
{/* 뷰 전환 토글 */}
|
{/* 뷰 전환 토글 */}
|
||||||
<div className="flex rounded-lg border border-wing-border overflow-hidden">
|
<div className="flex rounded-lg border border-wing-border overflow-hidden">
|
||||||
<button
|
<button
|
||||||
@ -215,7 +234,7 @@ export default function BypassConfig() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{searchTerm && (
|
{(searchTerm || selectedDomain) && (
|
||||||
<p className="mt-2 text-xs text-wing-muted">
|
<p className="mt-2 text-xs text-wing-muted">
|
||||||
{filteredConfigs.length}개 API 검색됨
|
{filteredConfigs.length}개 API 검색됨
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -15,8 +15,10 @@ public class WebViewController {
|
|||||||
@GetMapping({"/", "/jobs", "/executions", "/executions/{id:\\d+}",
|
@GetMapping({"/", "/jobs", "/executions", "/executions/{id:\\d+}",
|
||||||
"/recollects", "/recollects/{id:\\d+}",
|
"/recollects", "/recollects/{id:\\d+}",
|
||||||
"/schedules", "/schedule-timeline", "/monitoring",
|
"/schedules", "/schedule-timeline", "/monitoring",
|
||||||
|
"/bypass-config",
|
||||||
"/jobs/**", "/executions/**", "/recollects/**",
|
"/jobs/**", "/executions/**", "/recollects/**",
|
||||||
"/schedules/**", "/schedule-timeline/**", "/monitoring/**"})
|
"/schedules/**", "/schedule-timeline/**", "/monitoring/**",
|
||||||
|
"/bypass-config/**"})
|
||||||
public String forward() {
|
public String forward() {
|
||||||
return "forward:/index.html";
|
return "forward:/index.html";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,4 +33,7 @@ public class BypassParamDto {
|
|||||||
|
|
||||||
/** 정렬 순서 */
|
/** 정렬 순서 */
|
||||||
private Integer sortOrder;
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** Swagger @Parameter example 값 */
|
||||||
|
private String example;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,4 +74,10 @@ public class BypassApiParam {
|
|||||||
@Column(name = "sort_order", nullable = false)
|
@Column(name = "sort_order", nullable = false)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Integer sortOrder = 0;
|
private Integer sortOrder = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swagger @Parameter example 값
|
||||||
|
*/
|
||||||
|
@Column(name = "example", length = 200)
|
||||||
|
private String example;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -283,7 +283,9 @@ public class BypassCodeGenerator {
|
|||||||
String description = p.getDescription() != null ? p.getDescription() : p.getParamName();
|
String description = p.getDescription() != null ? p.getDescription() : p.getParamName();
|
||||||
String javaType = toJavaType(p.getParamType());
|
String javaType = toJavaType(p.getParamType());
|
||||||
String paramName = p.getParamName();
|
String paramName = p.getParamName();
|
||||||
String example = getDefaultExample(p.getParamType());
|
String example = (p.getExample() != null && !p.getExample().isEmpty())
|
||||||
|
? p.getExample()
|
||||||
|
: getDefaultExample(p.getParamType());
|
||||||
return switch (p.getParamIn().toUpperCase()) {
|
return switch (p.getParamIn().toUpperCase()) {
|
||||||
case "PATH" -> "@Parameter(description = \"" + description + "\", example = \"" + example + "\")\n"
|
case "PATH" -> "@Parameter(description = \"" + description + "\", example = \"" + example + "\")\n"
|
||||||
+ " @PathVariable " + javaType + " " + paramName;
|
+ " @PathVariable " + javaType + " " + paramName;
|
||||||
|
|||||||
@ -170,6 +170,7 @@ public class BypassConfigService {
|
|||||||
.required(dto.getRequired() != null ? dto.getRequired() : true)
|
.required(dto.getRequired() != null ? dto.getRequired() : true)
|
||||||
.description(dto.getDescription())
|
.description(dto.getDescription())
|
||||||
.sortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0)
|
.sortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0)
|
||||||
|
.example(dto.getExample())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +183,7 @@ public class BypassConfigService {
|
|||||||
.required(param.getRequired())
|
.required(param.getRequired())
|
||||||
.description(param.getDescription())
|
.description(param.getDescription())
|
||||||
.sortOrder(param.getSortOrder())
|
.sortOrder(param.getSortOrder())
|
||||||
|
.example(param.getExample())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user