Merge commit '5505791b6e3ef04480731b123512e90e9e035c43'

This commit is contained in:
2026-06-29 12:56:12 +09:00
135 changed files with 3155 additions and 938 deletions
@@ -0,0 +1,67 @@
@tailwind components;
@layer components {
.fileup_root {
@apply relative rounded-lg bg-background p-2;
}
.fileup_dropzone {
@apply border border-border outline-dashed outline-1 outline-border;
}
.fileup_panel {
@apply flex w-full flex-col items-center justify-center p-1;
}
.fileup_icon {
@apply h-4 w-5 text-label;
}
.fileup_hint {
@apply mb-1 text-sm text-label;
}
.fileup_hint_strong {
@apply font-semibold text-foreground;
}
.fileup_meta {
@apply text-xs text-label;
}
.fileup_item {
@apply text-foreground;
}
.fileup_item_active {
@apply bg-muted;
}
.fileup_item_name {
@apply flex h-full w-4/5 items-center gap-1.5 overflow-hidden font-medium leading-normal tracking-tight;
}
.fileup_item_remove {
@apply text-label;
}
.fileup_item_remove_icon {
@apply h-4 w-4 transition-colors duration-200 ease-in-out hover:stroke-destructive;
}
.fileup_input {
@apply border;
}
.fileup_input_idle {
@apply border-border;
}
.fileup_input_accept {
@apply border-primary;
}
.fileup_input_reject {
@apply border-destructive;
}
}
@@ -1,4 +1,6 @@
"use client";
import "./file-upload.css";
import { CloudUpload, Paperclip } from "lucide-react";
import { FileInput, FileUploader, FileUploaderContent, FileUploaderItem } from "./lib/file_upload";
@@ -36,19 +38,19 @@ export const FileUpload =
onValueChange={setFiles}
dropzoneOptions={dropZoneConfig}
acceptName={fileTypes}
className="relative bg-background rounded-lg p-2"
className="fileup_root"
>
<FileInput
id="fileInput"
className="outline-dashed outline-1 outline-slate-500"
className="fileup_dropzone"
>
<div className="flex items-center justify-center flex-col p-1 w-full ">
<CloudUpload className='text-gray-500 w-5 h-4' />
<p className="mb-1 text-sm text-gray-500 dark:text-gray-400">
<span className="font-semibold">클릭 후 업로드 </span>
<div className="fileup_panel">
<CloudUpload className='fileup_icon' />
<p className="fileup_hint">
<span className="fileup_hint_strong">클릭 후 업로드 </span>
&nbsp; 혹은 파일을 드래그 해주세요
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="fileup_meta">
{!fileTypes?"SVG, PNG, JPG, GIF":fileTypes}
</p>
</div>
@@ -344,14 +344,14 @@ export const FileUploaderItem = forwardRef<
ref={ref}
className={cn(
buttonVariants({ variant: "ghost" }),
"h-9 justify-between cursor-pointer relative w-full",
"fileup_item h-9 justify-between cursor-pointer relative w-full",
className,
isSelected ? "bg-muted" : "",
isSelected ? "fileup_item_active" : "",
)}
{...props}
>
<div className="font-medium leading-normal tracking-tight flex items-center gap-1.5 h-full !w-4/5 overflow-hidden "
<div className="fileup_item_name"
onClick={() =>{onFileClick?.(index)
}}>
{children}
@@ -359,7 +359,7 @@ export const FileUploaderItem = forwardRef<
<button
type="button"
className={cn(
"absolute",
"fileup_item_remove absolute",
"!w-1/5 z-50",
direction === "rtl"
? "left-1 top-1/2 -translate-y-1/2"
@@ -372,7 +372,7 @@ export const FileUploaderItem = forwardRef<
removeFileFromSet(index)}}
>
<span className="sr-only">remove item {index}</span>
<RemoveIcon className="w-4 h-4 hover:stroke-destructive duration-200 ease-in-out" />
<RemoveIcon className="fileup_item_remove_icon" />
</button>
</div>
);
@@ -396,14 +396,12 @@ export const FileInput = forwardRef<
>
<div
className={cn(
`w-full rounded-lg duration-300 ease-in-out
${
dropzoneState.isDragAccept
? "border-green-500"
: dropzoneState.isDragReject || isFileTooBig
? "border-red-500"
: "border-gray-300"
}`,
"fileup_input w-full rounded-lg duration-300 ease-in-out",
dropzoneState.isDragAccept && "fileup_input_accept",
(dropzoneState.isDragReject || isFileTooBig) && "fileup_input_reject",
!dropzoneState.isDragAccept &&
!(dropzoneState.isDragReject || isFileTooBig) &&
"fileup_input_idle",
className,
)}
{...rootProps}
@@ -0,0 +1,19 @@
@tailwind components;
@layer components {
.formui_label {
@apply text-xl;
}
.formui_option_wrap {
@apply float-right;
}
.formui_option {
@apply flex items-center gap-2;
}
.formui_option_label {
@apply w-28 text-xl text-label;
}
}
@@ -1,9 +1,11 @@
import "./form-ui.css";
import React, { useCallback, useEffect, useState } from "react";
import { FormControl, FormItem, FormLabel, FormMessage } from "../form";
import { Button, Checkbox } from "@common/components/ui";
import { Checkbox } from "@common/components/ui";
export const BZNM_TOT_NO = '종사업자등록번호';
export const CANDIDATE = '공급처 후보지 선택(필수아님)';
export const BZNM_TOT_NO = "종사업자등록번호";
export const CANDIDATE = "공급처 후보지 선택(필수아님)";
interface FormUiProps extends React.HTMLAttributes<HTMLDivElement> {
label?: string;
@@ -29,17 +31,19 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
}, [onCheckboxChange]);
return (
<FormItem ref={ref}>
{label && <FormLabel className="text-xl">{label}</FormLabel>}
{label && <FormLabel className="formui_label">{label}</FormLabel>}
{label === BZNM_TOT_NO && (
<>
<div className="float-right">
<div className="flex items-center gap-2">
<Checkbox
id="check"
<div className="formui_option_wrap">
<div className="formui_option">
<Checkbox
id="check"
checked={isChecked}
onCheckedChange={handleCheckboxChange}
/>
<label className="w-28 text-xl text-gray-600" htmlFor="check">해당없음</label>
<label className="formui_option_label" htmlFor="check">
해당없음
</label>
</div>
</div>
{children}
@@ -48,14 +52,14 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
{label === CANDIDATE && (
<FormControl>
{children}
</FormControl>
</FormControl>
)}
{label !== BZNM_TOT_NO && label !== CANDIDATE && (
<FormControl>
{React.isValidElement(children) && isDisabled
? React.cloneElement(children, {
? React.cloneElement(children, {
disabled: isDisabled,
...(children.props as FormElementProps)
...(children.props as FormElementProps),
})
: children}
</FormControl>
@@ -66,5 +70,5 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
}
);
FormUi.displayName = 'FormUi';
export { FormUi };
FormUi.displayName = "FormUi";
export { FormUi };
@@ -49,12 +49,14 @@ function Gis({
searchView: SearchView,
detailView: DetailView,
onFeatureSelect,
canSelectFeature,
setIsSearchModalOpen,
onCheckedLayersChange,
searchBar: SearchBar,
waterDepth: WaterDepth = null,
layerSettingView: LayerSettingView = null,
inlineLayerSettingView,
treeSectionClassName = "",
layerIconVisible,
isPlaying = false,
setIsPlaying = () => { },
@@ -73,6 +75,7 @@ function Gis({
zoomGuide,
showFloatingSearchButton = true,
treeTogglePosition = "center",
detachedTreeToggleButton = false,
showZoomScaleInfo = false,
}) {
const mapRef = useRef();
@@ -111,7 +114,7 @@ function Gis({
(data) => {
onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출
},
[mapInstanceRef],
[onFeatureSelect],
);
// 지도 생성 및 초기화
@@ -315,7 +318,9 @@ function Gis({
onProgressChange={handleProgressChange}
layerSettingView={LayerSettingView}
inlineLayerSettingView={inlineLayerSettingView}
treeSectionClassName={treeSectionClassName}
toggleButtonPosition={treeTogglePosition}
detachedToggleButton={detachedTreeToggleButton}
/>
{typeof WaterDepth === "function" && (
@@ -354,7 +359,7 @@ function Gis({
<GisZoomLevel zoomGuide={zoomGuide} showZoomScaleInfo={showZoomScaleInfo} />
)}
<GisMapToggle />
<GisSelect onFeatureSelect={handleFeatureSelect} />
<GisSelect onFeatureSelect={handleFeatureSelect} canSelectFeature={canSelectFeature} />
</div>
</div>
@@ -1,25 +1,43 @@
import React, { useContext, useEffect } from "react";
import React, { useContext, useEffect, useRef } from "react";
import { GisContext } from "./Gis";
import GisApp from "./lib/gis.core";
import GisOlSelect from "./lib/gis.select";
export default React.memo(function GisSelect({ onFeatureSelect }) {
export default React.memo(function GisSelect({ onFeatureSelect, canSelectFeature }) {
const { gisMap } = useContext(GisContext);
const onFeatureSelectRef = useRef(onFeatureSelect);
const canSelectFeatureRef = useRef(canSelectFeature);
useEffect(() => {
onFeatureSelectRef.current = onFeatureSelect;
}, [onFeatureSelect]);
useEffect(() => {
canSelectFeatureRef.current = canSelectFeature;
}, [canSelectFeature]);
useEffect(() => {
if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함
// 이미 select가 있으면 재생성하지 않음
const select = new GisOlSelect(gisMap);
GisApp.Module.select = select;
// 피처 선택 시 콜백 등록
select.getSelectedInfo(function (features) {
if (features?.[0]) {
onFeatureSelect(features[0]);
const isSelectable = canSelectFeatureRef.current?.(features[0]);
if (isSelectable === false) {
select.resetSelectedFeature?.();
return;
}
onFeatureSelectRef.current?.(features[0]);
}
});
}, [gisMap, onFeatureSelect]);
return () => {
select.removeInteration?.();
};
}, [gisMap]);
// const select = new GisOlSelect(gisMap);
// GisApp.Module.select = select;
@@ -29,6 +29,7 @@ import GisApp from "./lib/gis.core";
* - onProgressChange(total, current): 애니메이션 진행상태 콜백
* - layerSettingView: (선택) 좌측 레이어 트리 하단에 표시할 설정 영역
* - layerIconVisible: (선택) 레이어 행 우측 액션 아이콘 노출 여부를 결정하는 함수
* - detachedToggleButton: (선택) 접기/펼치기 버튼을 패널 밖 고정 핸들로 노출할지 여부
*/
export default React.memo(function GisTree({
checkedNodes = [],
@@ -46,7 +47,10 @@ export default React.memo(function GisTree({
layerSettingView: LayerSettingView = null,
layerIconVisible,
inlineLayerSettingView,
treeSectionClassName = "",
toggleButtonPosition = "center",
showLayerPanelTitle = true,
detachedToggleButton = false,
}) {
const { gisCore, treeData } = useGisContext();
@@ -442,10 +446,11 @@ export default React.memo(function GisTree({
* 레이어 정보로부터 범례 이미지 경로 또는 동적 생성 이미지를 반환합니다.
*/
const makeLegendSrc = (layerinfo) => {
if (!layerinfo.gobjTy) return null;
if (layerinfo.iconNm) {
return GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm;
} else {
}
if (!layerinfo.gobjTy) return null;
else {
return makeLegendImage(layerinfo).src;
}
};
@@ -512,7 +517,10 @@ export default React.memo(function GisTree({
if (!treeData || treeData.length === 0) return null;
const toggleButtonPositionClass =
toggleButtonPosition === "top" ? "top-[5.5rem]" : "top-1/2 -translate-y-1/2";
toggleButtonPosition === "top" ? "top-4" : "top-1/2 -translate-y-1/2";
const thinScrollbarClass =
"[scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]";
const treeSectionClassNames = [treeSectionClassName, thinScrollbarClass].filter(Boolean).join(" ");
return (
<>
@@ -524,13 +532,18 @@ export default React.memo(function GisTree({
}`}
>
<div
className={`grow overflow-y-auto border-none p-2 pt-0 shadow-none ${activeTab === "layers" ? "block" : "hidden"
className={`grow overflow-y-auto border-none p-2 ${showLayerPanelTitle ? "pt-0" : "pt-2"} shadow-none ${activeTab === "layers" ? "block" : "hidden"
}`}
>
<h3 className="gis-layer-panel-title mx-2 my-4 text-3xl font-bold text-gray-600 text-center">시설물</h3>
<hr className="mb-4" />
{showLayerPanelTitle && (
<>
<h3 className="gis-layer-panel-title mx-2 my-4 text-3xl font-bold text-gray-600 text-center">시설물</h3>
<hr className="mb-4" />
</>
)}
{/* DnD Tree */}
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<div className={treeSectionClassNames || undefined}>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree
tree={nodes}
rootId={null}
@@ -542,6 +555,13 @@ export default React.memo(function GisTree({
(child) =>
child.parent === node.id && child.data.exprsYn === "Y"
);
const nodeLabel = String(node?.data?.lyrNm ?? "").trim();
const isSinkholeRootTreeNode =
String(node.id) === "SN_SINKHOLE" ||
nodeLabel === "지반침하정보";
const isFloodHistoryRootTreeNode = nodeLabel === "침수흔적도";
const hideTreeLegendIcon =
isSinkholeRootTreeNode || isFloodHistoryRootTreeNode;
const showLayerActionButton = layerIconVisible?.(node);
const showLayerActionIcon = depth > 0 && imageVisible;
@@ -550,9 +570,10 @@ export default React.memo(function GisTree({
return (
<div style={{ display: isVisible ? "block" : "none" }}>
<div
className="gis-layer-row flex items-center gap-2 rounded px-2 py-1 hover:bg-gray-100"
className="gis-layer-row flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-gray-100"
style={{
marginInlineStart: depth * 16,
width: `calc(100% - ${depth * 16}px)`,
}}
>
{hasVisibleChildren ? (
@@ -563,7 +584,7 @@ export default React.memo(function GisTree({
title={isOpen ? "닫기" : "열기"}
/>
) : (
<span style={{ display: "inline-block", width: 16 }} />
<span className="shrink-0" style={{ display: "inline-block", width: 16 }} />
)}
<input
type="checkbox"
@@ -571,14 +592,17 @@ export default React.memo(function GisTree({
onChange={() => handleCheck(node.id)}
className="cursor-pointer"
/>
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">
{!hideTreeLegendIcon && node.icon && <span>{node.icon}</span>}
<span
className="min-w-0 flex-1 truncate whitespace-nowrap text-[1.3rem] font-medium text-label"
title={String(node.text ?? "")}
>
{node.text}
</span>
{showLayerActionButton && (
<button
type="button"
className="gis-layer-action-button ml-1 shrink-0 rounded-sm border border-[#9cc6f0] bg-white px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-[#2c6fb7] transition-colors hover:bg-[#eef6ff]"
className="gis-layer-action-button ml-auto shrink-0 rounded-sm border border-[#9cc6f0] bg-white px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-[#2c6fb7] transition-colors hover:bg-[#eef6ff]"
onClick={() =>
handleLayerImgClick(
node.data.lyrTyNm,
@@ -624,26 +648,41 @@ export default React.memo(function GisTree({
return aOrder - bOrder;
}}
/>
</DndProvider>
</DndProvider>
</div>
</div>
{LayerSettingView && (
<div className="gis-layer-setting shrink-0 bg-white px-2 pb-3">
<LayerSettingView />
</div>
)}
{!detachedToggleButton && (
<button
onClick={() => setVisible(false)}
className={`gis-layer-toggle absolute right-[-3px] z-50 transform rounded-l-none rounded-r border border-l border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`}
>
<ChevronLeft size={16} />
</button>
)}
</div>
{detachedToggleButton && visible && (
<button
onClick={() => setVisible(false)}
className={`gis-layer-toggle absolute right-[-3px] z-50 transform rounded-l-none rounded-r border border-l border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`}
className={`gis-layer-toggle absolute left-[250px] z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] text-[#1e88e5] shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-[#eaf4ff] ${toggleButtonPositionClass}`}
title="접기"
>
<ChevronLeft size={16} />
<ChevronLeft className="h-4 w-4" />
</button>
</div>
)}
{!visible && (
<button
onClick={() => setVisible(true)}
className={`gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`}
className={detachedToggleButton
? `gis-layer-toggle absolute left-0 z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] text-[#1e88e5] shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-[#eaf4ff] ${toggleButtonPositionClass}`
: `gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`}
title="열기"
>
<ChevronRight size={16} />
{detachedToggleButton ? <ChevronRight className="h-4 w-4" /> : <ChevronRight size={16} />}
</button>
)}
</>
@@ -45,7 +45,7 @@ GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${G
// NGII 정보(배경지도)
//GisApp.NGII_WMTS_URL = process.env.NEXT_PUBLIC_NGII_URL;
GisApp.NGII_WMTS_URL = `/ngii-server/openapi/Gettile.do?apikey=${process.env.NEXT_PUBLIC_NGII_PARAM}`;
GisApp.NGII_WMTS_PARAM = process.env.NEXT_PUBLIC_NGII_PARAM ?? '';
GisApp.EMAP_URL = '/emap/';
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
@@ -79,6 +79,17 @@ GisApp.setLayerInfos = function (layerInfos) {
GisApp.layerInfos = layerInfos;
};
GisApp.getNgiiWmtsUrl = function (layerName, matrix, x, y) {
const layerAwareParams = /(^|&)layer=[^&]*/i.test(GisApp.NGII_WMTS_PARAM)
? GisApp.NGII_WMTS_PARAM.replace(/(^|&)layer=[^&]*/i, `$1layer=${layerName}`)
: `${GisApp.NGII_WMTS_PARAM}&layer=${layerName}`;
return `/ngii-server/openapi/Gettile.do?apikey=${layerAwareParams}`
.replace("{z}", matrix)
.replace("{x}", x)
.replace("{y}", y);
};
/**
* GisApp core
* @requires gis.layer.js
@@ -161,7 +172,7 @@ GisApp.core.prototype = {
});
const wmtsSource = new TileWMTS({
//url: GisApp.NGII_WMTS_URL,
// url은 setTileUrlFunction에서 layer별로 동적으로 만든다.
projection: projection,
tileGrid: tileGrid,
crossOrigin: "anonymous",
@@ -175,11 +186,7 @@ GisApp.core.prototype = {
wmtsSource.setTileUrlFunction((tileCoord) => {
const [z, x, y] = tileCoord;
const matrix = matrixIds[z];
// console.log(GisApp.NGII_WMTS_URL.replace("{z}", matrix).replace("{x}", x).replace("{y}", y));
return GisApp.NGII_WMTS_URL
.replace("{z}", matrix)
.replace("{x}", x)
.replace("{y}", y);
return GisApp.getNgiiWmtsUrl(_layer, matrix, x, y);
});
return new TileLayer({
@@ -742,10 +742,13 @@ GisOlLayer.prototype = {
const extent = this.combineExtents(geometries.map(g => g?.getExtent()));
let map = GisApp.Module.core.map;
const targetZoom = 16;
const currentZoom = Number(map.getView().getZoom());
const maxZoom = Number.isFinite(currentZoom) && currentZoom > targetZoom ? currentZoom : targetZoom;
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16
maxZoom: maxZoom
});
}
} else {
@@ -132,13 +132,15 @@ GisOlSelect.prototype = {
const selectedLayer = e.target?.getLayer(feature) || feature.getLayer?.();
const clckZoom = Number(selectedLayer?.get('clckZoom'));
const targetZoom = Number.isFinite(clckZoom) && clckZoom > 0 ? clckZoom : 16;
const currentZoom = Number(map.getView().getZoom());
const maxZoom = Number.isFinite(currentZoom) && currentZoom > targetZoom ? currentZoom : targetZoom;
const geometry = feature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: targetZoom
maxZoom: maxZoom
});
}
}
@@ -343,12 +343,13 @@ GisOlStyle.prototype = {
//레이어 스타일 반환
getLayerStyle: function (layer, options) {
let gobjTy = layer.get('gobjTy');
if (!gobjTy) return;
if (!gobjTy || typeof this[gobjTy] !== 'function') return;
let style = this[gobjTy](layer, options);
return style;
},
getLayerStyleFunction: function (layer, options) {
let gobjTy = layer.get('gobjTy');
if (!gobjTy || typeof this[gobjTy] !== 'function') return;
let styleFunction = function (feature, resolution) {
return this[gobjTy](layer, options, feature, resolution);
}.bind(this);
@@ -1,3 +1,5 @@
import "./search-icon.css";
export default function SearchIcon() {
return (
<svg
@@ -6,7 +8,7 @@ export default function SearchIcon() {
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6 text-gray-700"
className="searchicon_svg"
>
<path
strokeLinecap="round"
@@ -15,4 +17,4 @@ export default function SearchIcon() {
/>
</svg>
);
}
}
@@ -0,0 +1,7 @@
@tailwind components;
@layer components {
.searchicon_svg {
@apply h-6 w-6 text-label;
}
}
@@ -1,5 +1,8 @@
import "./search-input.css";
import { Input, Label } from "@common/components";
import React from "react";
import { cn } from "../../lib/utils";
const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label:string, textValue:string, setText:(val:string)=>void}>(({className, label, textValue, setText}, ref) => {
const changeText=(e:React.ChangeEvent<HTMLInputElement>)=>{
@@ -20,9 +23,9 @@ const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label:
}
return (
<div className={className+" flex items-center"} >
{label && <Label className="w-20 text-xl text-gray-600">{label}</Label>}
<Input className="!text-lg flex-1 w-3/4 h-13 border rounded" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input>
<div className={cn("searchinput_root", className)} >
{label && <Label className="searchinput_label">{label}</Label>}
<Input ref={ref} className="searchinput_field" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input>
</div>
)
});
@@ -0,0 +1,15 @@
@tailwind components;
@layer components {
.searchinput_root {
@apply flex items-center;
}
.searchinput_label {
@apply w-20 text-xl;
}
.searchinput_field {
@apply h-14 w-3/4 flex-1 rounded-md border border-input text-lg;
}
}
@@ -1,6 +1,9 @@
import "./search-input-select.css";
import { Input, Label } from "@common/components";
import React from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../select/lib/selectBox";
import { cn } from "../../lib/utils";
export type SelectDataType = {
text: string;
val: string;
@@ -23,12 +26,12 @@ const SearchInputSelect = React.forwardRef<HTMLInputElement, {className:string,
}
return (
<div className={className+" flex items-center"} >
{label && <Label className="w-20 text-xl text-gray-600">{label}</Label>}
<div className="flex w-full items-center">
<Input className="w-2/3 text-xl h-14 p-2 border rounded" value={textValue} onChange={changeText} />
<div className={cn("searchinputselect_root", className)} >
{label && <Label className="searchinputselect_label">{label}</Label>}
<div className="searchinputselect_controls">
<Input ref={ref} className="searchinputselect_input" value={textValue} onChange={changeText} />
<Select onValueChange={onSelectValue} value={selectValue}>
<SelectTrigger className="ml-2 w-1/3" >
<SelectTrigger className="searchinputselect_select" >
<SelectValue placeholder={selectData?.[0]?.text ?? ""} />
</SelectTrigger>
{/* <SelectContent ref={refs ? (refs as unknown as RefType)?.selectValue : null}> */}
@@ -0,0 +1,23 @@
@tailwind components;
@layer components {
.searchinputselect_root {
@apply flex items-center;
}
.searchinputselect_label {
@apply w-20 text-xl;
}
.searchinputselect_controls {
@apply flex w-full items-center;
}
.searchinputselect_input {
@apply h-14 w-2/3 rounded-md border border-input p-2 text-xl;
}
.searchinputselect_select {
@apply ml-2 w-1/3;
}
}
@@ -1,3 +1,5 @@
import "./select.css";
// selectBox
import { Label } from "@common/components";
@@ -10,6 +12,7 @@ import {
SelectTrigger,
SelectValue,
} from "./lib/selectBox";
import { cn } from "../../lib/utils";
/** 차트 기본 스타일 */
interface SelectBox1 {
@@ -43,7 +46,7 @@ const SelectBox = React.forwardRef<
value,
readOnly = false,
selectContentClass,
labelClass = "w-20 whitespace-nowrap text-xl text-gray-600",
labelClass = "w-20 whitespace-nowrap text-xl",
},
ref,
) => {
@@ -72,9 +75,9 @@ const SelectBox = React.forwardRef<
: undefined;
return (
<div className={className + " flex items-center"} ref={ref}>
<div className={cn("selectbox_root", className)} ref={ref}>
{label && (
<Label className={labelClass}>
<Label className={cn("selectbox_label", labelClass)}>
{label}
</Label>
)}
@@ -92,9 +95,7 @@ const SelectBox = React.forwardRef<
value={currentValue}
// disabled={readOnly} //물재이용 감리 지적사항으로 일단 주석처리
>
<SelectTrigger
className={selectClass ? selectClass : "flex-1 rounded border p-2"}
>
<SelectTrigger className={cn("selectbox_trigger", selectClass)}>
<SelectValue placeholder={placeHolder} />
</SelectTrigger>
<SelectContent className={selectContentClass}>
@@ -0,0 +1,15 @@
@tailwind components;
@layer components {
.selectbox_root {
@apply flex items-center;
}
.selectbox_label {
@apply text-label;
}
.selectbox_trigger {
@apply flex-1 rounded-md border border-input bg-form px-3 py-2;
}
}
@@ -1,46 +1,61 @@
import { Tabs, TabsList, TabsTrigger } from '@common/components/ui';
import React from 'react';
import "./tab.css";
import { Tabs, TabsList, TabsTrigger } from "@common/components/ui";
import React from "react";
import { cn } from "../../lib/utils";
interface TabItem {
value: string;
label: string;
}
interface UITabProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'dir'> {
interface UITabProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> {
defaultValue?: string;
tabList: TabItem[];
children?: React.ReactNode;
onValueChange?: (value: string) => void;
variant?: "default" | "underline" | "outline";
}
const UITab = React.forwardRef<
HTMLDivElement,
UITabProps
>(({defaultValue, tabList, children, onValueChange, ...props}, ref) => {
return (
<Tabs
defaultValue={defaultValue || tabList[0]?.value}
className={"w-full space-x-1 border-gray-200"}
onValueChange={onValueChange}
ref={ref}
{...props}
>
<TabsList className="flex space-x-1">
{tabList.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="py-2 px-4 text-sm font-medium border-b-2 focus:outline-none data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 data-[state=inactive]:border-transparent data-[state=inactive]:text-gray-500 hover:text-gray-700 hover:border-gray-300"
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{children}
</Tabs>
);
});
const UITab = React.forwardRef<HTMLDivElement, UITabProps>(
(
{
defaultValue,
tabList,
children,
onValueChange,
className,
variant = "default",
...props
},
ref,
) => {
return (
<Tabs
variant={variant}
defaultValue={defaultValue || tabList[0]?.value}
className={cn("w-full", variant === "underline" && "btab_root", className)}
onValueChange={onValueChange}
ref={ref}
{...props}
>
<TabsList className={cn(variant === "underline" && "btab_list")}>
{tabList.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className={cn(variant === "underline" && "btab_trigger")}
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{children}
</Tabs>
);
},
);
UITab.displayName = 'UITab';
UITab.displayName = "UITab";
export { UITab };
@@ -0,0 +1,27 @@
@tailwind components;
@layer components {
.btab_root {
@apply w-full;
}
.btab_list {
@apply flex space-x-1 border-b border-border bg-transparent p-0;
}
.btab_trigger {
@apply rounded-none border-b-2 border-transparent bg-transparent px-4 py-2 text-sm font-medium text-label;
}
.btab_root .btab_trigger[data-state="active"] {
@apply border-primary text-primary;
}
.btab_root .btab_trigger[data-state="inactive"] {
@apply text-label;
}
.btab_root .btab_trigger:hover {
@apply border-border text-foreground;
}
}
@@ -29,7 +29,7 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
colSpan={head.colSpan || 1}
rowSpan={head.rowSpan || 1}
style={{ whiteSpace: 'pre-line' }}
className="bg-gray-50"
className=""
>
{head.title}
</TableHead>
@@ -22,4 +22,6 @@
.tabs[data-variant="outline"] .tabs-trigger {
@apply relative rounded-none rounded-t-sm border-border bg-background text-muted-foreground after:absolute after:bottom-0 after:left-0 after:h-[0.1rem] after:w-full after:bg-border after:content-[''] data-[state=active]:border data-[state=active]:border-b-0 data-[state=active]:border-border data-[state=active]:text-foreground data-[state=active]:after:bg-transparent;
}
}