Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 설정 데이터 내보내기/가져오기 #344

Merged
merged 17 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/client/src/components/atom/form/TextArea.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
export let id: string;
export let value: string;
export let label: string;
export let showLabel: boolean = false;
export let labelClass: string = '';
export let inputClass: string = '';
export let invalidClass: string = '';
export let invalidText: string = '이 값은 올바르지 않습니다.';
let clazz = '';
export { clazz as class };
</script>

<div class="{clazz} flex flex-col">
<!--suppress XmlInvalidId -->
<label class="{labelClass} mb-1 block font-bold" for={id} class:hidden={!showLabel}
>{label ?? ''} <span class:hidden={!$$props.required} class="text-red-800">*</span></label>
<textarea
{id}
class="{inputClass} resize-none grow rounded-md border-transparent bg-gray-100 transition-all
invalid:outline-red-600 hover:bg-gray-200 focus:bg-white disabled:bg-gray-300 disabled:text-gray-400"
bind:value
{...$$restProps} />
<p class="{invalidClass} mt-1 hidden">{invalidText}</p>
</div>

<style lang="postcss">
input:invalid ~ p {
@apply block;
}
</style>
2 changes: 1 addition & 1 deletion packages/client/src/components/molecule/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
<Dismiss />
</button>
</div>
<div class="mx-4 my-2 h-auto grow overflow-y-auto overflow-x-visible">
<div class="mx-4 my-2 h-auto grow overflow-y-auto overflow-x-visible p-1">
<slot />
</div>
<div class="mx-4 flex shrink-0 justify-end gap-3 pb-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script lang="ts">
import Modal from '../../Modal.svelte';
import { config } from '$lib/store';
import DocumentArrowRight from '../../../../icons/DocumentArrowRight.svelte';
import { createEventDispatcher } from 'svelte';
import { DepartmentConfigSchema, getDepartmentConfigs } from '$lib/api/config';
import TextGrammarCheckmark from '../../../../icons/TextGrammarCheckmark.svelte';
import TextArea from 'src/components/atom/form/TextArea.svelte';
import { z } from 'zod';
import Checkmark from 'src/icons/Checkmark.svelte';
import Dismiss from 'src/icons/Dismiss.svelte';

const dispatch = createEventDispatcher<{
submit: DepartmentConfigResponse[];
}>();

$: departments = $config && $config.success ? getDepartmentConfigs($config.result) : [];

export let open = false;

const title = '학과(부) 설정 가져오기/내보내기';
let configText;
$: if(departments && !configText) {
configText = JSON.stringify(departments, null, 2);
}
let green = false;
let error = '';
$: if(configText) {
error = '';
green = false;
}

function checkConfigJSON(): boolean {
try {
const parsed = z.array(DepartmentConfigSchema).safeParse(JSON.parse(configText));
if(parsed.success == false) {
error = JSON.stringify(parsed.error, null, 2);
}
return parsed.success;
} catch (e) {
console.error(e.message);
error = e.message;
return false;
}
}

function updateConfigs() {
const parsed = z.array(DepartmentConfigSchema).safeParse(JSON.parse(configText));
if(parsed.success) {
const configs = parsed.data;
const configResponse: DepartmentConfigResponse[] = configs.map((conf) => {
const { id, name, activateFrom, activateTo, contact } = conf;
return {
id,
name,
...(activateFrom
? { activate_from: activateFrom.toISOString() }
: { activate_from: null }),
...(activateTo
? { activate_to: activateTo.toISOString() }
: { activate_to: null }),
...(contact ? { contact } : { contact: null }),
};
});
dispatch('submit', configResponse);
}
}
</script>

<Modal
on:close
on:click:secondary={() => green = checkConfigJSON()}
on:click={updateConfigs}
{title}
bind:open
primaryDisabled={!green}
primaryText="가져오기"
secondaryText="검증"
isPrimaryBtnIconRight
isSecondaryBtnIconRight
{...$$restProps}>
<div class="flex flex-col gap-3 h-full">
<TextArea
class="h-full"
id="json"
bind:value={configText}
label="서비스 설정 데이터"
showLabel
/>
<p class="text-gray-800 text-sm">데이터를 복사하여 저장하거나, 직접 수정하여 서비스에 적용할 수 있습니다.</p>
{#if green}
<div class="text-green-800 flex items-center gap-1">
<Checkmark class="w-3" />
<p class="text-xs font-bold">검증 성공!</p>
</div>
{:else if error}
<div class="text-red-800 flex items-center gap-1">
<Dismiss class="w-3" />
<p class="text-xs font-bold">검증 실패</p>
</div>
<pre class="rounded-md p-1 bg-white text-red-800 whitespace-pre-wrap text-xs font-mono">{error}</pre>
{:else}
<p class="text-xs">데이터를 가져오려면 검증하세요.</p>
{/if}
</div>
<DocumentArrowRight slot="primaryIcon" />
<TextGrammarCheckmark slot="secondaryIcon" />
</Modal>
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@
import DepartmentConfigEditor from './DepartmentConfigEditor.svelte';
import Skeleton from '../../../atom/Skeleton.svelte';
import { apiDeleteConfig, apiUpdateConfig, getDepartmentConfigs } from '$lib/api/config';
import Code from '../../../../icons/Code.svelte';
import DepartmentImportExportModal from './DepartmentImportExportModal.svelte';
import Button from '../../../atom/Button.svelte';

$: configs = $config && $config.success ? getDepartmentConfigs($config.result) : [];

let updating = false;
$: if ($config) updating = false;
let importExportModalOpen = false;

function openImportExportModal() {
importExportModalOpen = true;
}

function deleteDepartment(event: CustomEvent<ConfigDeleteRequest>) {
updating = true;
Expand Down Expand Up @@ -43,10 +51,40 @@
updating = false;
});
}

function importDepartmentConfigs(event: CustomEvent<DepartmentConfigResponse[]>) {
importExportModalOpen = false;
const importedConfig = event.detail;
updating = true;
const updatePromises = importedConfig.map((conf) => apiUpdateConfig(conf as DepartmentConfigUpdateRequest).then((res) => {
if(res.success == false) {
throw (res as ErrorResponse<LockerError>).error;
}
return true;
}));
Promise.all(updatePromises)
.then(() => {
updating = false;
config.refresh();
})
.catch((err) => {
console.error(err);
updating = false;
});
}
</script>

<div class="my-8 flex flex-col gap-3 lg:mx-8">
<h3 class="mx-6 lg:mx-0">학과(부)별 설정</h3>
<div class="my-8 flex w-auto flex-col item-stretch gap-3 lg:mx-8">
<div class="mx-6 flex w-full flex-wrap lg:mx-0">
<h3>학과(부)별 설정</h3>
{#if !updating}
<div class="flex grow items-center justify-end gap-1">
<Button on:click={openImportExportModal} class="bg-gray-200 text-gray-700" isIconRight>
<Code slot="icon" />
</Button>
</div>
{/if}
</div>
{#if $config && $config.success && !updating}
<div class="flex flex-col gap-3 bg-white p-6 shadow-md lg:rounded-md">
<DepartmentConfigEditor on:delete={deleteDepartment} on:update={updateDepartment} {configs} />
Expand All @@ -57,3 +95,8 @@
<Skeleton class="h-[32rem] w-full grow bg-gray-200 lg:rounded-md" />
{/if}
</div>

<DepartmentImportExportModal
bind:open={importExportModalOpen}
on:close={() => (importExportModalOpen = false)}
on:submit={importDepartmentConfigs} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script lang="ts">
import Modal from '../../Modal.svelte';
import { config } from '$lib/store';
import DocumentArrowRight from '../../../../icons/DocumentArrowRight.svelte';
import { createEventDispatcher } from 'svelte';
import { ServiceConfigSchema, getServiceConfig } from '$lib/api/config';
import TextGrammarCheckmark from '../../../../icons/TextGrammarCheckmark.svelte';
import TextArea from 'src/components/atom/form/TextArea.svelte';
import Checkmark from 'src/icons/Checkmark.svelte';
import Dismiss from 'src/icons/Dismiss.svelte';

const dispatch = createEventDispatcher<{
submit: ServiceConfigResponse;
}>();

$: serviceConfig = $config && $config.success ? getServiceConfig($config.result) : {};

export let open = false;

const title = '서비스 설정 가져오기/내보내기';
let configText;
$: if(serviceConfig && !configText) {
configText = JSON.stringify(serviceConfig, null, 2);
}
let green = false;
let error = '';
$: if(configText) {
error = '';
green = false;
}

function checkConfigJSON(): boolean {
try {
const parsed = ServiceConfigSchema.safeParse(JSON.parse(configText));
if(parsed.success == false) {
error = JSON.stringify(parsed.error, null, 2);
}
return parsed.success;
} catch (e) {
console.error(e.message);
error = e.message;
return false;
}
}

function updateConfigs() {
const parsed = ServiceConfigSchema.safeParse(JSON.parse(configText));
if(parsed.success) {
const { name, activateFrom, activateTo, alert, buildings } = parsed.data as ServiceConfig;
const configResponse: ServiceConfigResponse = {
id: 'SERVICE',
name,
...(activateFrom
? { activate_from: activateFrom.toISOString() }
: { activate_from: null }),
...(activateTo
? { activate_to: activateTo.toISOString() }
: { activate_to: null }),
...(alert ? { alert } : { alert: null }),
buildings,
}
dispatch('submit', configResponse);
}
}
</script>

<Modal
on:close
on:click:secondary={() => green = checkConfigJSON()}
on:click={updateConfigs}
{title}
bind:open
primaryDisabled={!green}
primaryText="가져오기"
secondaryText="검증"
isPrimaryBtnIconRight
isSecondaryBtnIconRight
{...$$restProps}>
<div class="flex flex-col gap-3 h-full">
<TextArea
class="h-full"
id="json"
bind:value={configText}
label="서비스 설정 데이터"
showLabel
/>
<p class="text-gray-800 text-sm">데이터를 복사하여 저장하거나, 직접 수정하여 서비스에 적용할 수 있습니다.</p>
{#if green}
<div class="text-green-800 flex items-center gap-1">
<Checkmark class="w-3" />
<p class="text-xs font-bold">검증 성공!</p>
</div>
{:else if error}
<div class="text-red-800 flex items-center gap-1">
<Dismiss class="w-3" />
<p class="text-xs font-bold">검증 실패</p>
</div>
<pre class="rounded-md p-1 bg-white text-red-800 whitespace-pre-wrap text-xs font-mono">{error}</pre>
{:else}
<p class="text-xs">데이터를 가져오려면 검증하세요.</p>
{/if}
</div>
<DocumentArrowRight slot="primaryIcon" />
<TextGrammarCheckmark slot="secondaryIcon" />
</Modal>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import UpdateScreen from '../../../atom/UpdateScreen.svelte';
import Warning from '../../../../icons/Warning.svelte';
import Skeleton from '../../../atom/Skeleton.svelte';
import Code from 'src/icons/Code.svelte';
import ServiceImportExportModal from './ServiceImportExportModal.svelte';

let updating = false;

Expand All @@ -19,6 +21,11 @@
let activateTo: Date;
let alert: string;
let buildings: { [buildingNum: string]: Building };
let importExportModalOpen = false;

function openImportExportModal() {
importExportModalOpen = true;
}

$: serviceConfig = $config && $config.success ? getServiceConfig($config.result) : undefined;

Expand Down Expand Up @@ -66,6 +73,25 @@
});
}

function importServiceConfig(event: CustomEvent<ServiceConfigResponse>) {
importExportModalOpen = false;
const importedConfig = event.detail;
updating = true;
apiUpdateConfig(importedConfig as ServiceConfigUpdateRequest)
.then((res) => {
updating = false;
if (res.success) {
config.refresh();
} else {
console.error((res as ErrorResponse<LockerError>).error);
}
})
.catch((err) => {
console.error(err);
updating = false;
});
}

$: if (serviceConfig) {
initializeValues();
updating = false;
Expand Down Expand Up @@ -102,6 +128,12 @@
저장
<SaveEdit slot="icon" />
</Button>
<Button
on:click={openImportExportModal}
class="bg-gray-200 text-gray-700"
isIconRight>
<Code slot="icon" />
</Button>
</div>
</div>
{#if serviceConfig && !isServiceReady}
Expand Down Expand Up @@ -193,3 +225,8 @@
</section>
{/if}
</div>

<ServiceImportExportModal
bind:open={importExportModalOpen}
on:close={() => (importExportModalOpen = false)}
on:submit={importServiceConfig} />
Loading
Loading