@@ -78,6 +84,7 @@ const handleInput = (event: Event) => {
diff --git a/frontend/src/shared/ui/components/KsTextField.vue b/frontend/src/shared/ui/components/KsTextField.vue
index 5db57213..bdf37a0e 100644
--- a/frontend/src/shared/ui/components/KsTextField.vue
+++ b/frontend/src/shared/ui/components/KsTextField.vue
@@ -1,5 +1,7 @@
@@ -77,6 +89,7 @@ const handleInput = (event: Event) => {
{
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
+ @keydown="handleKeyPress"
/>
@@ -119,6 +133,32 @@ const handleInput = (event: Event) => {
gap: var(--spacing-2);
}
+.ks-text-field--inline {
+ flex-direction: row;
+ align-items: center;
+ gap: var(--ks-space-2, 6px);
+}
+
+.ks-text-field--inline .ks-text-field__label {
+ width: var(--ks-field-label-width, 72px);
+ min-width: var(--ks-field-label-width, 72px);
+ max-width: var(--ks-field-label-width, 72px);
+ text-align: left;
+ white-space: nowrap;
+ flex-shrink: 0;
+ margin-bottom: 0;
+}
+
+.ks-text-field--inline .ks-text-field__container {
+ flex: 1 1 auto;
+ min-width: 0;
+ max-width: 100%;
+ width: 100%;
+}
+
+
+
+
.ks-text-field--sm {
--input-height: 32px;
--font-size: var(--font-size-sm);
diff --git a/frontend/src/shared/ui/components/index.ts b/frontend/src/shared/ui/components/index.ts
index 34698073..1cd2f263 100644
--- a/frontend/src/shared/ui/components/index.ts
+++ b/frontend/src/shared/ui/components/index.ts
@@ -24,3 +24,5 @@ export { default as KsFormSection } from './KsFormSection.vue'
export { default as KsFormSpan } from './KsFormSpan.vue'
export { default as KsValidationSummary } from './KsValidationSummary.vue'
export { default as KsSplitter } from './KsSplitter.vue'
+export { default as KArtsellMetricCard } from './KArtsellMetricCard.vue'
+
diff --git a/frontend/src/shared/ui/composables/useFormFieldNavigation.ts b/frontend/src/shared/ui/composables/useFormFieldNavigation.ts
new file mode 100644
index 00000000..30fa814d
--- /dev/null
+++ b/frontend/src/shared/ui/composables/useFormFieldNavigation.ts
@@ -0,0 +1,91 @@
+import { ref, onMounted } from 'vue'
+
+export function useFormFieldNavigation() {
+ const inputRef = ref(null)
+
+ function findFormElement(): HTMLFormElement | null {
+ if (!inputRef.value) return null
+ return inputRef.value.closest('form')
+ }
+
+ function getFormInputElements(): HTMLElement[] {
+ const form = findFormElement()
+ if (!form) return []
+
+ const selectors = [
+ 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"])',
+ 'textarea',
+ '[role="button"][tabindex="0"]',
+ 'select'
+ ]
+
+ return Array.from(form.querySelectorAll(selectors.join(',')))
+ .filter((el: Element): el is HTMLElement => {
+ if (el instanceof HTMLInputElement) {
+ return !el.disabled && !el.readonly && el.type !== 'hidden'
+ }
+ if (el instanceof HTMLTextAreaElement) {
+ return !el.disabled && !el.readonly
+ }
+ if (el instanceof HTMLSelectElement) {
+ return !el.disabled && !el.readonly
+ }
+ return !el.hasAttribute('disabled')
+ })
+ }
+
+ function findNextField(): HTMLElement | null {
+ const elements = getFormInputElements()
+ if (!inputRef.value || elements.length === 0) return null
+
+ const currentIndex = elements.indexOf(inputRef.value)
+ if (currentIndex === -1) return null
+
+ const nextIndex = currentIndex + 1
+ return nextIndex < elements.length ? elements[nextIndex] : elements[0]
+ }
+
+ function moveToNextField(): void {
+ const nextField = findNextField()
+ if (nextField) {
+ nextField.focus()
+ // 如果是 input/textarea,select all
+ if (nextField instanceof HTMLInputElement || nextField instanceof HTMLTextAreaElement) {
+ nextField.select?.()
+ }
+ }
+ }
+
+ function handleKeyDown(event: KeyboardEvent, isTextArea: boolean = false): boolean {
+ if (!isTextArea) {
+ // Input/Select: Enter → 다음 필드
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ moveToNextField()
+ return true
+ }
+ } else {
+ // TextArea: Ctrl+Enter → 줄바꿈, Enter → 다음 필드
+ if (event.key === 'Enter') {
+ if (event.ctrlKey || event.metaKey) {
+ // Ctrl+Enter: 줄바꿈 (기본 동작)
+ return false
+ } else {
+ // Enter: 다음 필드
+ event.preventDefault()
+ moveToNextField()
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ return {
+ inputRef,
+ handleKeyDown,
+ moveToNextField,
+ findNextField,
+ getFormInputElements
+ }
+}
diff --git a/frontend/src/shared/ui/contracts/layoutContext.ts b/frontend/src/shared/ui/contracts/layoutContext.ts
new file mode 100644
index 00000000..84f12088
--- /dev/null
+++ b/frontend/src/shared/ui/contracts/layoutContext.ts
@@ -0,0 +1,20 @@
+import { inject, provide, computed, type InjectionKey, type Ref } from 'vue'
+
+export type KsFieldLayoutMode = 'filter' | 'form' | 'inline' | 'stacked'
+
+export const KS_FIELD_LAYOUT_KEY: InjectionKey[> = Symbol('KS_FIELD_LAYOUT_KEY')
+
+export function provideFieldLayout(mode: Ref | KsFieldLayoutMode): void {
+ const modeRef = computed(() => (typeof mode === 'string' ? mode : mode.value))
+ provide(KS_FIELD_LAYOUT_KEY, modeRef)
+}
+
+export function useFieldLayout(explicitInline?: boolean): { isInline: Ref } {
+ const parentLayout = inject(KS_FIELD_LAYOUT_KEY, null)
+ const isInline = computed(() => {
+ if (explicitInline !== undefined) return explicitInline
+ if (parentLayout) return parentLayout.value === 'filter' || parentLayout.value === 'inline'
+ return false
+ })
+ return { isInline }
+}
diff --git a/frontend/src/shared/ui/contracts/screenContract.ts b/frontend/src/shared/ui/contracts/screenContract.ts
index 907c5045..5dd16939 100644
--- a/frontend/src/shared/ui/contracts/screenContract.ts
+++ b/frontend/src/shared/ui/contracts/screenContract.ts
@@ -27,4 +27,7 @@ export interface StandardScreenProps {
evidence?: ScreenEvidenceContext
warning?: string
error?: Error | null
+ initialRatio?: number
+ storageKey?: string
}
+
diff --git a/frontend/src/shared/ui/layouts/CrudWorkspaceLayout.vue b/frontend/src/shared/ui/layouts/CrudWorkspaceLayout.vue
index c2f01c56..285164ba 100644
--- a/frontend/src/shared/ui/layouts/CrudWorkspaceLayout.vue
+++ b/frontend/src/shared/ui/layouts/CrudWorkspaceLayout.vue
@@ -1,3 +1,64 @@
-
-
-
+
+
+
+
+
+
+
+
diff --git a/frontend/src/shared/ui/layouts/DashboardLayout.vue b/frontend/src/shared/ui/layouts/DashboardLayout.vue
index c00e2a90..f4e9bc51 100644
--- a/frontend/src/shared/ui/layouts/DashboardLayout.vue
+++ b/frontend/src/shared/ui/layouts/DashboardLayout.vue
@@ -1,2 +1,79 @@
-
-
+
+
+
+
+
diff --git a/frontend/src/shared/ui/layouts/FormPageLayout.vue b/frontend/src/shared/ui/layouts/FormPageLayout.vue
index fa7f00f0..8a291a9d 100644
--- a/frontend/src/shared/ui/layouts/FormPageLayout.vue
+++ b/frontend/src/shared/ui/layouts/FormPageLayout.vue
@@ -5,4 +5,30 @@
]
-
+
+
diff --git a/frontend/src/shared/ui/layouts/PageLayout.vue b/frontend/src/shared/ui/layouts/PageLayout.vue
index b78c6de8..59b14549 100644
--- a/frontend/src/shared/ui/layouts/PageLayout.vue
+++ b/frontend/src/shared/ui/layouts/PageLayout.vue
@@ -1,8 +1,11 @@
+
+