diff --git a/src/lib/repository/programRepository.js b/src/lib/repository/programRepository.js
new file mode 100644
index 0000000..bdceb9a
--- /dev/null
+++ b/src/lib/repository/programRepository.js
@@ -0,0 +1,51 @@
+import { supabase } from "../supabaseClient";
+
+class ProgramRepository {
+ async fetchTotalProcutCount() {
+ const { count, error } = await supabase.from("program").select("*", { count: "exact" });
+
+ if (error) throw new Error(error.message);
+ return count;
+ }
+
+ async fetchProgramById({ programId }) {
+ if (programId === "undefined") return { program: {} };
+
+ const { data: program, error } = await supabase
+ .from("program")
+ .select("*, product(*), act(*)")
+ .eq("id", programId)
+ .order("createdAt", {
+ foreignTable: "act",
+ ascending: true,
+ })
+ .maybeSingle();
+
+ if (error) throw new Error(error.message);
+
+ return { program };
+ }
+
+ async fetchProgramByPaging({ pageNum = 1 }) {
+ let page = (Number(pageNum) - 1) * 20;
+
+ if (page < 0) page = 0;
+
+ const { data: programs, error } = await supabase
+ .from("program")
+ .select("*")
+ .order("createdAt", { ascending: true })
+ .range(page, page + 19);
+
+ if (error) throw new Error(error.message);
+
+ return { programs };
+ }
+
+ async deleteProgramById({ programId }) {
+ const { error } = await supabase.from("program").delete().in("id", programId);
+ if (error) throw new Error(error.message);
+ }
+}
+
+export default new ProgramRepository();
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 0b48d77..507f33b 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -30,6 +30,9 @@
상품
+ 프로그램
쿠폰
diff --git a/src/routes/program/+page.server.js b/src/routes/program/+page.server.js
new file mode 100644
index 0000000..92ff9e7
--- /dev/null
+++ b/src/routes/program/+page.server.js
@@ -0,0 +1,10 @@
+import programRepository from "../../lib/repository/programRepository.js";
+
+export const load = async ({ url }) => {
+ const pageNum = url.searchParams.get("pageNum") || 1;
+
+ const { programs } = await programRepository.fetchProgramByPaging({ pageNum });
+ const totalProgramCount = await programRepository.fetchTotalProcutCount();
+
+ return { programs, totalProgramCount };
+};
diff --git a/src/routes/program/+page.svelte b/src/routes/program/+page.svelte
new file mode 100644
index 0000000..8897c6b
--- /dev/null
+++ b/src/routes/program/+page.svelte
@@ -0,0 +1,152 @@
+
+
+
+
+
+ {#if totalProgramCount > 0 && !isNaN(Number(pageNum))}
+
+
+ {#if Number(pageNum) !== 1}
+
+ {/if}
+
+ {#if Number(pageNum) < Math.ceil(totalProgramCount / 20)}
+
+ {/if}
+
+
+ {/if}
+
+
+
+
+
+
diff --git a/src/routes/program/+server.js b/src/routes/program/+server.js
new file mode 100644
index 0000000..9f41274
--- /dev/null
+++ b/src/routes/program/+server.js
@@ -0,0 +1,17 @@
+import { json } from "@sveltejs/kit";
+import programRepository from "../../lib/repository/programRepository.js";
+
+export const DELETE = async ({ request }) => {
+ try {
+ const programs = await request.json();
+
+ const programIds = programs.map((err) => err.id);
+
+ await programRepository.deleteProgramById({ programId: programIds });
+
+ return json(true);
+ } catch (err) {
+ console.error("product/server.js Error : ", err);
+ return json(false);
+ }
+};
diff --git a/src/routes/program/[programId]/+page.server.js b/src/routes/program/[programId]/+page.server.js
new file mode 100644
index 0000000..633bc57
--- /dev/null
+++ b/src/routes/program/[programId]/+page.server.js
@@ -0,0 +1,105 @@
+import programRepository from "../../../lib/repository/programRepository.js";
+import { supabase } from "../../../lib/supabaseClient.js";
+
+export const load = async ({ params }) => {
+ const programId = params.programId;
+
+ if (programId === "null") {
+ const program = {};
+ return { program };
+ }
+
+ const { program } = await programRepository.fetchProgramById({ programId });
+
+ return { program };
+};
+
+export const actions = {
+ save: async ({ request, params }) => {
+ const product = await request.formData();
+
+ // 기본 정보.
+ const basicInfo = {
+ name: product.get("name"),
+ isComingSoon: product.get("isComingSoon"),
+ duration: product.get("duration"),
+ feature: product.get("feature"),
+ infoText: product.get("infoText"),
+ ingredient: product.get("ingredient"),
+ manual: product.get("manual"),
+ };
+
+ if (product.get("bannerImage") === "no") basicInfo.bannerImageUrl = null;
+ else if (product.get("bannerImage") === "url") {
+ basicInfo.bannerImageUrl = product.get("bannerImageUrl");
+ } else {
+ const file = product.get("bannerImageFile");
+ // 파일 등록 후 url. 등록.
+ const { data, error } = await supabase.storage
+ .from("program-images")
+ .upload(`img/${params.programId}/banner/${file.name}`, file, {
+ cacheControl: "3600",
+ upsert: true,
+ });
+
+ let { data: imageUrl } = await supabase.storage.from("program-images").getPublicUrl(data.path);
+ imageUrl = imageUrl.publicUrl;
+ basicInfo.bannerImageUrl = imageUrl;
+ }
+
+ if (product.get("productImage") === "no") basicInfo.productImageUrl = null;
+ else if (product.get("productImage") === "url") {
+ basicInfo.productImageUrl = product.get("productImageUrl");
+ } else {
+ const file = product.get("productImageFile");
+ const { data, error } = await supabase.storage
+ .from("program-images")
+ .upload(`img/${params.programId}/product/${file.name}`, file, {
+ cacheControl: "3600",
+ upsert: true,
+ });
+ let { data: imageUrl } = await supabase.storage.from("program-images").getPublicUrl(data.path);
+ imageUrl = imageUrl.publicUrl;
+ basicInfo.productImageUrl = imageUrl;
+ }
+
+ if (product.get("bannerComingSoonImage") === "no") basicInfo.bannerImageUrlComingSoon = null;
+ else if (product.get("bannerComingSoonImage") == "url") {
+ basicInfo.bannerImageUrlComingSoon = product.get("bannerComingSoonImageUrl");
+ } else {
+ const file = product.get("bannerComingSoonImageFile");
+ const { data, error } = await supabase.storage
+ .from("program-images")
+ .upload(`img/${params.programId}/bannerComingSoon/${file.name}`, file, {
+ cacheControl: "3600",
+ upsert: true,
+ });
+
+ let { data: imageUrl } = await supabase.storage.from("program-images").getPublicUrl(data.path);
+ imageUrl = imageUrl.publicUrl;
+ basicInfo.bannerImageUrlComingSoon = imageUrl;
+ }
+
+ if (params.programId === "null") {
+ const { error } = await supabase.from("program").insert({ ...basicInfo });
+
+ if (error) {
+ console.error("Program Save(Insert) Err", error.message);
+ return "Fail";
+ }
+
+ return "Success";
+ }
+ const { error } = await supabase
+ .from("program")
+ .update({ ...basicInfo })
+ .eq("id", params.programId);
+
+ if (error) {
+ console.error("Program Save Err", error.message);
+ return "Fail";
+ }
+
+ return "Success";
+ },
+};
diff --git a/src/routes/program/[programId]/+page.svelte b/src/routes/program/[programId]/+page.svelte
new file mode 100644
index 0000000..e787252
--- /dev/null
+++ b/src/routes/program/[programId]/+page.svelte
@@ -0,0 +1,238 @@
+
+
+
+
+
+
+
diff --git a/src/routes/program/[programId]/act/+page.server.js b/src/routes/program/[programId]/act/+page.server.js
new file mode 100644
index 0000000..032d086
--- /dev/null
+++ b/src/routes/program/[programId]/act/+page.server.js
@@ -0,0 +1,64 @@
+import programRepository from "../../../../lib/repository/programRepository.js";
+import { supabase } from "../../../../lib/supabaseClient.js";
+
+export const load = async ({ params }) => {
+ const programId = params.programId;
+
+ if (programId === "null") {
+ const program = {};
+ return { program };
+ }
+
+ const { program } = await programRepository.fetchProgramById({ programId });
+
+ return { program };
+};
+
+export const actions = {
+ save: async ({ request, params }) => {
+ const acts = await request.formData();
+
+ const actList = [];
+ const length = acts.get("length");
+
+ for (let i = 0; i < length; i++) {
+ let act = {};
+ if (acts.get(`${i}imageFile`) !== null) {
+ // 이미지를 업로드해야 하는 경우.
+ const file = acts.get(`${i}imageFile`);
+ const { data, error } = await supabase.storage
+ .from("program-images")
+ .upload(`img/${params.programId}/act/${i}+${file.name}`, file, {
+ cacheControl: "3600",
+ upsert: true,
+ });
+
+ if (error) throw new Error(error.message);
+
+ let { data: imageUrl } = await supabase.storage.from("program-images").getPublicUrl(data.path);
+ imageUrl = imageUrl.publicUrl;
+ act.imageUrl = imageUrl;
+ } else {
+ act.imageUrl = acts.get(`${i}imageUrl`);
+ }
+
+ act.title = acts.get(`${i}title`);
+ act.subTitle = acts.get(`${i}subTitle`);
+ act.duration = acts.get(`${i}duration`);
+ act.isOvertime = acts.get(`${i}isOvertime`);
+ act.order = acts.get(`${i}order`);
+ act.programId = params.programId;
+
+ actList.push(act);
+ }
+
+ const { error: deleteErr } = await supabase.from("act").delete().eq("programId", params.programId);
+ const { error: insertErr } = await supabase.from("act").insert(actList);
+
+ console.log(insertErr);
+ if (deleteErr) throw new Error(deleteErr.message);
+ if (insertErr) throw new Error(insertErr.message);
+
+ return "Success";
+ },
+};
diff --git a/src/routes/program/[programId]/act/+page.svelte b/src/routes/program/[programId]/act/+page.svelte
new file mode 100644
index 0000000..eebc8e9
--- /dev/null
+++ b/src/routes/program/[programId]/act/+page.svelte
@@ -0,0 +1,226 @@
+
+
+
+
Program
+ {program.name}
+
+
+
+{#if isModalOpen}
+
+{/if}
diff --git a/src/routes/program/[programId]/act/+server.js b/src/routes/program/[programId]/act/+server.js
new file mode 100644
index 0000000..0fbe2ad
--- /dev/null
+++ b/src/routes/program/[programId]/act/+server.js
@@ -0,0 +1 @@
+import { supabase } from "../../../../lib/supabaseClient.js";
diff --git a/src/routes/user/[userId]/+page.svelte b/src/routes/user/[userId]/+page.svelte
index 44ae76b..8374cdb 100644
--- a/src/routes/user/[userId]/+page.svelte
+++ b/src/routes/user/[userId]/+page.svelte
@@ -61,7 +61,6 @@
const contact = [["일시", "분류", "문의 내용", "답변 내용", "처리"]];
- console.log(user.contact);
user.contact.forEach((err) => {
contact.push([
err.createdAt ? new Date(err.createdAt).toLocaleString() : "",