feat: replace mapping text input with company dropdown selector
All checks were successful
Deploy to Production / deploy (push) Successful in 1m11s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m11s
- Add proxy endpoint GET /api/admin/digital-twin/companies - Fetch company list from digital-twin API on modal open - Select dropdown shows company name + industry type - Remove manual UUID input in favor of selection
This commit is contained in:
@@ -480,6 +480,19 @@ a {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.loading-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
font-size: 13px;
|
||||
color: var(--md-on-surface-variant);
|
||||
}
|
||||
|
||||
.loading-inline .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* ===== Login ===== */
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { useTenants } from '@/lib/hooks';
|
||||
import { Card } from '@/components/Card';
|
||||
import { api } from '@/lib/api';
|
||||
import { Tenant } from '@/lib/types';
|
||||
import type { Tenant } from '@/lib/types';
|
||||
|
||||
interface DigitalTwinCompany {
|
||||
companyId: string;
|
||||
name: string;
|
||||
industryType: string;
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const { user, logout } = useAuth();
|
||||
@@ -25,6 +31,17 @@ export default function HomePage() {
|
||||
companyId: '',
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [dtCompanies, setDtCompanies] = useState<DigitalTwinCompany[]>([]);
|
||||
const [companiesLoading, setCompaniesLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mappingModal.open) return;
|
||||
setCompaniesLoading(true);
|
||||
api.get<{ companies: DigitalTwinCompany[] }>('/api/admin/digital-twin/companies')
|
||||
.then((res) => setDtCompanies(res.companies))
|
||||
.catch(() => setDtCompanies([]))
|
||||
.finally(() => setCompaniesLoading(false));
|
||||
}, [mappingModal.open]);
|
||||
|
||||
const handleTenantSelect = (tenantId: string) => {
|
||||
router.push(`/${tenantId}`);
|
||||
@@ -136,30 +153,30 @@ export default function HomePage() {
|
||||
|
||||
<div className="modal-body-form">
|
||||
<div className="form-field">
|
||||
<label>디지털 트윈 회사 ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={mappingModal.companyId}
|
||||
onChange={(e) => setMappingModal((prev) => ({ ...prev, companyId: e.target.value }))}
|
||||
placeholder="회사 UUID (예: 7f5c058c-...)"
|
||||
autoFocus
|
||||
/>
|
||||
<label>디지털 트윈 회사</label>
|
||||
{companiesLoading ? (
|
||||
<div className="loading-inline">
|
||||
<span className="material-symbols-outlined spinning">progress_activity</span>
|
||||
회사 목록 로딩 중...
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={mappingModal.companyId}
|
||||
onChange={(e) => setMappingModal((prev) => ({ ...prev, companyId: e.target.value }))}
|
||||
autoFocus
|
||||
>
|
||||
<option value="">선택 안 함</option>
|
||||
{dtCompanies.map((c) => (
|
||||
<option key={c.companyId} value={c.companyId}>
|
||||
{c.name} ({c.industryType})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
{mappingModal.companyId && (
|
||||
<button
|
||||
className="btn-outline btn-danger"
|
||||
onClick={() => {
|
||||
setMappingModal((prev) => ({ ...prev, companyId: '' }));
|
||||
}}
|
||||
disabled={isSaving}
|
||||
style={{ marginRight: 'auto' }}
|
||||
>
|
||||
매핑 해제
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn-text"
|
||||
onClick={() => setMappingModal((prev) => ({ ...prev, open: false }))}
|
||||
|
||||
33
main.py
33
main.py
@@ -155,6 +155,39 @@ async def update_tenant_digital_twin_mapping(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/admin/digital-twin/companies")
|
||||
async def list_digital_twin_companies(
|
||||
current_user: TokenData = Depends(require_superadmin),
|
||||
):
|
||||
import httpx
|
||||
|
||||
api_url = os.getenv("DIGITAL_TWIN_API_URL", "")
|
||||
if not api_url:
|
||||
raise HTTPException(
|
||||
status_code=503, detail="DIGITAL_TWIN_API_URL not configured"
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(f"{api_url}/api/v1/companies")
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
companies = data.get("data", [])
|
||||
return {
|
||||
"companies": [
|
||||
{
|
||||
"companyId": c.get("companyId"),
|
||||
"name": c.get("name"),
|
||||
"industryType": c.get("industryType"),
|
||||
}
|
||||
for c in companies
|
||||
]
|
||||
}
|
||||
except httpx.HTTPError as e:
|
||||
raise HTTPException(
|
||||
status_code=502, detail=f"Failed to fetch from digital-twin: {e}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
Reference in New Issue
Block a user