git clone https://github.com/01Republic/8g.git
git pullDB_HOST=localhost
DB_PORT=3306
DB_USER=myuser
DB_PASSWORD=mypassword
DB_NAME=mydb3. ๊ทธ๋ฆฌ๊ณ migration๋ฅผ ๋ฐ๋ก ์ถ๊ฐํ์ง ์์๊ธฐ ๋๋ฌธ์ ์๋ ์ฟผ๋ฆฌ ๋ค๋ก ํด๋น ํจํค์ง์ ํ์ํ ํ ์ด๋ธ์ ๋ง๋ญ๋๋ค
- ์ด ๋ถ๋ถ์ ์ฐจํ ๋ ผ์ ํ์ / ํ ์ด๋ธ ์ด๋ฆ ๋ฐ ๋ง์ด๊ทธ๋ ์ด์ ์ฌ๋ถ ๋ฑ๋ฑ
CREATE TABLE IF NOT EXISTS integration_app_form_metadata (
id INT NOT NULL AUTO_INCREMENT,
product_id INT NOT NULL,
meta JSON NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
UNIQUE KEY uq_integration_app_form_metadata_product_id (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;pnpm run dev์ฐ์ ๊ธฐ๋ณธ์ ์ผ๋ก 8g๋ scodi user & org ํ ์ด๋ธ์ ๊ฐ์ด ์๋๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฐ์ธ ์๋น์ค๋ผ๊ธฐ ๋ณด๋จ ์กฐ์ง ๋จ์ ์๋น์ค ์ด๊ธฐ ๋๋ฌธ์ slug๋ฅผ ํ์ฉํด์ subdomain url์ ์ ๊ณตํฉ๋๋ค! ๋ฐ๋ผ์ org์ slug๋ฅผ ์์์ผํฉ๋๋ค!! ์ด ๋ถ๋ถ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ organizations์ slug ์ปฌ๋ผ์ ํ์ธํด์ฃผ์ธ์!
- (์ถ๊ฐ๋ก slug๋ ํ์ฌ ์ค์ฝ๋ ๋ด์์ ์์ ํ ์ ์์ด์ ๋๋น๋ก ํ์ธํด์ผํฉ๋๋ค. ๋๋ {name}-{id} ๋ฐฉ์์ผ๋ก ์๋ ์์ฑ๋๊ธฐ ๋๋ฌธ์ ๊ทธ๊ฑฐ๋ก ํด์ฃผ์ ๋ ๋ฉ๋๋ค.)
- ๊ทธ๋ฆฌ๊ณ subdomain์ ์์ด๋ก๋ง ํด์ผํฉ๋๋ค
- ๋ฉํฐ ํ ๋ํธ ๋ฐฉ์
๊ทธ๋ฌ๋ฉด ์๋์ ๊ฐ์ด ๋ก๊ทธ์ธ ํ๋ฉด์ด ๋น๋๋ค.
๋ก๊ทธ์ธ ํ์๋ ๋ค์๊ณผ ๊ฐ์ด ํ ํ๋ฉด์ผ๋ก ์ด๋ํฉ๋๋ค. ์ฌ๊ธฐ์๋ ๋ด๊ฐ ๋ฑ๋กํ ์ฑ(scordi api์์๋ subscription ๊ฐ๋ )์ ๋ณด์ฌ์ฃผ๊ณ ๊ฒ์ํ ์ ์์ต๋๋ค. ๊ฒ์ ๊ฒฐ๊ณผ๋ ์๋์ ๋์ ๊ตฌ๋ ์ฑ ์ชฝ์์ ์นด๋ ํ์์ผ๋ก ๋ณด์ฌ์ฃผ๊ฒ ๋ฉ๋๋ค.
- ์ถ๊ฐ๋ก ํ์ฌ ์นด๋์ product ์ด๋ฆ๊ณผ ์์ด์ฝ ๋ฑ์ ๋ณด์ฌ์ฃผ๋๋ฐ, ์ด๋ ๊ฒ ๋ณด์ฌ์ฃผ๋ฉด ์ด๋ค ๊ตฌ๋ ์ธ์ง ์ ์ ์์ด์ ์ถ๊ฐ ์ ๋ณด๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ด ํ์
์ข์ธก์ apps ๋ฒํผ์ ํด๋ฆญํ๋ฉด ์ด๋ํ๋ ํ์ด์ง๋ก ๋ด๊ฐ ๋ฑ๋กํ ์ฑ(subscription) ๋ฆฌ์คํธ๋ฅผ ๋ณด์ฌ์ค๋๋ค.
ํด๋ฆญํ๋ฉด ๋ชจ๋ฌ์ด๋ ์ ํญ์ผ๋ก ์ ๋ณด๋ฅผ ๋ณด์ฌ์ค์ผํ๋๋ฐ ์์ง ์ด๋ค ์ ๋ณด๋ฅผ ๋ณด์ฌ์ค์ผํ ์ง ์ ํด์ง์ง ์์์ ๋ฐ๋ก ๊ตฌํ์ด ๋์ง ์์์ต๋๋ค.
์ผ์ชฝ ์๋จ์ + integration apps ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋์ค๋ ํ๋ฉด์ผ๋ก ์ค์ ์ฐ๋์ด ๊ฐ๋ฅํ product๊ฐ ๋์ค๊ฒ ๋ฉ๋๋ค. ์ฌ๊ธฐ์ ๋์ค๋ product๋ scordi์ product table์์ ๊ฐ์ ธ์ค๋ ๊ฑฐ๊ณ ์ฌ๊ธฐ ํ๋ก์ ํธ์์ ์๋ก ๋ง๋ ํ ์ด๋ธ integration_app_form_metadata ์์ is_active ๊ฐ true์ธ ๊ฒ๋ค์ ๊ฐ์ ธ์์ ๊ฑฐ๊ธฐ์ ์๋ product id ๋ค๋ก ๊ฐ์ ธ์ต๋๋ค
๊ทธ๋ฆฌ๊ณ Connect ๋ฒํผ์ ํด๋ฆญํ๋ฉด integration_app_form_metadata์ ์๋ metadata๋ก ํผ ๋ชจ๋ฌ์ ๋์์ ์ฐ๋์ ์งํํ๊ฒ ๋ฉ๋๋ค.
Connect ๋ฒํผ์ ํด๋ฆญํ์ ๋ ๋์ค๋ ํผ ๋ชจ๋ฌ๋ฅผ ์ค์ ํ ์ ์๋ ํ์ด์ง ์ ๋๋ค. ๊ฐ ๋ชจ๋ฌ์ ์์ ๋ฐ ํ๋ฉด์ ์น์ ์ด๋ผ ์ ์ํ๊ณ , ์น์ ์ ์ถ๊ฐํ ๋ ๋ง๋ค ์คํ ์ด ์ถ๊ฐ ๋ฉ๋๋ค.
๊ฐ ์น์ ์ ํ์ดํ๊ณผ ์ฌ๋ฌ ๊ฐ์ ์์ ํ ์ ์๊ณ , workflow๋ฅผ ์์ ํ ์ ์์ต๋๋ค. ๋ค๋ง ์์ง ์ํฌํ๋ก์ฐ ์ ๋ ฅ ์ชฝ์ ๊ตฌํ์ด ๋ ๋์ด ์์ด์ ์์ง ๋์ํ์ง ์์ต๋๋ค! ์ฐจํ workflow ํ์ด์ง๋ฅผ ํ์ฉํ์ฌ ๊ตฌ์ถํ ์์ ์ ๋๋ค.
๋ง์ง๋ง์ผ๋ก ์ค์ integration apps์์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด์ ์ฐ์ธก ์๋จ์ Active ํ ๊ธ์ ํจ ํ ์ ์ฅ ๋ฒํผ์ ํด๋ฆญํด์ฃผ์๋ฉด ์ค์ ๋ก ๋ฐ์์ด ๋์ ๋ณด์ ๋๋ค.
์ํฌํ๋ก์ฐ๋ฅผ ๋ง๋ค ์ ์๋ ํ๋ฉด์ฐ๋ก ์ฐ์ธก ์๋จ์ add block์ ํด๋ฆญํ๋ฉด ์ค์ drag and drop ๋ฐฉ์์ผ๋ก ๋ ธ๋ ๋ผ๋ฆฌ ์ฐ๊ฒฐํด์ ์ค์ ํฌ๋กค๋ง์ ํ ์ ์๋ ์ํฌํ๋ก์ฐ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
์ค์ Run workflow์ ํด๋ฆญํ๋ฉด ์คํ์ด ๋๋ฉฐ ์๋จ์ ์๋ url์ด ํ๊ฒ url์ด ๋์ ๋์ํ๊ฒ ๋ฉ๋๋ค.
.
โโ app/ # ์ฑ ์์ค (Remix/React)
โ โโ assets/ # ์ ์ ๋ฆฌ์์ค ํฌํผ
โ โโ client/ # ๋ผ์ฐํธ๋ณ ํด๋ผ์ด์ธํธ ๋ทฐ ๋ ์ด์ด
โ โ โโ admin/ # ํผ/์ํฌํ๋ก์ฐ ๋น๋ ๋ฑ ๊ด๋ฆฌ ๋๊ตฌ
โ โ โโ private/ # ๋ก๊ทธ์ธ ์ดํ ํ๋ฉด(Home, Apps, Integration ๋ฑ)
โ โ โโ public/ # ๋ก๊ทธ์ธ ์ด์ ํ๋ฉด(Login ๋ฑ)
โ โโ components/ # ๊ณต์ฉ ์ปดํฌ๋ํธ(shadcn ๊ธฐ๋ฐ UI ํฌํจ)
โ โโ hooks/ # ์ปค์คํ
ํ
โ โโ layouts/ # ๋ ์ด์์ ์ปดํฌ๋ํธ
โ โโ lib/ # ์ ํธ๋ฆฌํฐ
โ โโ middleware/ # ํด๋ผ์ด์ธํธ/๋ผ์ฐํธ ๋ฏธ๋ค์จ์ด(auth ๋ฑ)
โ โโ models/ # ๋๋ฉ์ธ ๋ก์ง(Integration/Workflow ๋ฑ)
โ โโ routes/ # Remix ๋ผ์ฐํธ ์๋ํฌ์ธํธ
โ โโ root.tsx # ์ฑ ์ํธ๋ฆฌ
โ โโ routes.ts # React Router ๋ผ์ฐํธ ์ค์ (ํ์ผ ๊ธฐ๋ฐ โ Config ๊ธฐ๋ฐ ์ ํ)
โ โโ session.ts # ์ธ์
์ ํธ
โโ public/ # ์ ์ ํ์ผ ๋ฃจํธ(ํ๋น์ฝ ๋ฑ)
โโ build/ # ๋น๋ ๊ฒฐ๊ณผ๋ฌผ
โโ components.json # shadcn/ui ๊ตฌ์ฑ
โโ vite.config.ts # Vite ์ค์
โโ tsconfig.json # TS ์ค์ - ๋ผ์ฐํธ ์ํธ๋ฆฌ๋
app/routes.ts์์ ๋ ์ด์์ ๊ธฐ๋ฐ์ผ๋ก ์ ์ธํฉ๋๋ค. ๊ณตํต ๋ ์ด์์(layouts/sidebar,layouts/common)์ ๋ฐ๋ผ child ๋ผ์ฐํธ๊ฐ ๋ฌถ์ ๋๋ค. - ๋ผ์ฐํธ ๊ตฌํ์ฒด๋
app/routes/*.tsx์ ์์นํ๋ฉฐ, ์ฌ๊ธฐ์ ๋ฉํ/loader/action ๋ฑ์ ์ ์ํฉ๋๋ค. - ํด๋ผ์ด์ธํธ ์ ์ฉ ํ์ด์ง๋
app/client์ ๋ถ๋ฆฌํด ๋ก๋๋ค. Remix ๋ผ์ฐํธ ํ์ผ์์๋ ์ด ์ปดํฌ๋ํธ๋ฅผ ๋ถ๋ฌ์ ๊ตฌ์ฑ๋ง ํฉ๋๋ค. - ๋ก๊ทธ์ธ ์ดํ ํ๋ฉด์
app/client/private, ๋ก๊ทธ์ธ ์ด์ ํ๋ฉด์app/client/public, ๋น๋/๊ด๋ฆฌ ํ๋ฉด์app/client/admin์ ๋๋ ๊ด๋ฆฌํฉ๋๋ค.
- loader: ์๋ฒ์์๋ง ์คํ๋๋ ๋ฐ์ดํฐ ํจ์นญ. ๋ฐฉ๋ฌธ ์ ํ์ํ ์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ๋ชจ์๋๋ค. ์)
integration.tsx์์ ์ฐ๋ ๊ฐ๋ฅํ product ๋ฆฌ์คํธ/๋ฉํ๋ฐ์ดํฐ ๋ก๋ฉ. - action: ํผ ์ ์ถ/๋ฎคํ
์ด์
์ฒ๋ฆฌ. ์๋ฒ์์ ์คํ๋๋ฉฐ DB ์ฐ๋ ๋ฑ ๋ถ์ํจ๊ณผ๋ฅผ ๋ด๋นํฉ๋๋ค. ์)
home.tsx์์ ๊ฒ์ ํผ ์ ์ถ ํ ๊ฒฐ๊ณผ ๋ฆฌ์คํธ ์๋ต. - clientLoader / clientAction: React Router 7์ ํด๋ผ์ด์ธํธ ์ ์ฉ ๋ฐ์ดํฐ API. ์๋ฒ round-trip ์์ด ๋ธ๋ผ์ฐ์ ์ํ๋ง์ผ๋ก ์ฒ๋ฆฌํ ์์ ์ ์ฌ์ฉํฉ๋๋ค(์ถํ ์บ์ฑ์ด๋ purely client-side ๋ฐ์ดํฐ๊ฐ ํ์ํ ๋ ๋์ ์์ ).
// app/routes/integration.tsx
export async function loader({}: Route.LoaderArgs) {
return await findActiveIntegrationProducts();
}
// app/routes/home.tsx
export async function action({ request, context }: Route.ActionArgs) {
const user = context.get(userContext);
const formData = await request.formData();
const query = formData.get("query")?.toString();
const apps = await findAllApp({ query, orgId: user!.orgId });
return { apps };
}app/models/integration์ ์ฐ๋ ๊ด๋ จ ํ์ /์คํ๊ธฐ/์ฌ๋ ๋ฉํ๋ฐ์ดํฐ ๊ด๋ฆฌ๊ฐ ์์ต๋๋ค. ์ค์ ํผ ๋ฉํ(integration_app_form_metadata)๋ฅผ ์ฝ์ด UI ๋ชจ๋ฌ์ ๊ตฌ์ฑํ๋ ํ๋ฆ์ ๊ฐ์ง๋๋ค.app/models/workflow์๋ ์ํฌํ๋ก์ฐ UI ๋ ธ๋ ์ปดํฌ๋ํธ ๋ฐ ์คํ ๊ด๋ จ ๋ฒ ์ด์ค ๋ก์ง์ด ์ ๋ฆฌ๋์ด ์์ต๋๋ค.
- ๊ธฐ๋ณธ UI๋
app/components/ui์ shadcn/ui๋ฅผ ๋ฒ ์ด์ค๋ก ์ปค์คํ ํ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค. ๊ฐ๋ฅํ ํ ๊ณต์ฉ ์ปดํฌ๋ํธ๋ก ๋์ด์ฌ๋ ค ์ค๋ณต์ ์ค์ ๋๋ค. - ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ณตํฉ ์ปดํฌ๋ํธ(์:
Reorderable,stepper)๋app/components๋ฃจํธ ํ์์ ์์ต๋๋ค.
- ์ธ์
๊ด๋ จ ๋ก์ง์
app/session.ts์ ๋ชจ์๋๊ณ , ๊ฒฝ๋ก ๋ณดํธ๋app/middleware/auth.ts๋ฅผ ํตํด ์ฒ๋ฆฌํฉ๋๋ค.
- ํ์ฌ ๋ง์ด๊ทธ๋ ์ด์
๋๊ตฌ๋ ๋ถ์ฌ๋์ง ์์๊ณ , ํ์ ํ
์ด๋ธ
integration_app_form_metadata๋ README์ SQL๋ก ์์ฑํฉ๋๋ค. - ํด๋น ํ
์ด๋ธ์
meta(JSON) ๊ธฐ๋ฐ์ผ๋ก Integration ๋ชจ๋ฌ/์น์ ๊ตฌ์ฑ์ด ๋์ ์ผ๋ก ์ด๋ค์ง๋๋ค.
pnpm add scordi-extension
# or
npm install scordi-extensionimport { EightGClient } from "scordi-extension";
const client = new EightGClient();
await client.checkExtension();const workflow = {
version: "1.0",
start: "collectProduct",
steps: [
{
id: "collectProduct",
block: {
name: "get-text",
selector: ".product-title",
findBy: "cssSelector",
useTextContent: true,
option: {},
},
switch: [
{
when: {
equals: {
left: "$.steps.collectProduct.result.data",
right: "ํ์ ",
},
},
next: "handleSoldOut",
},
],
next: "goToDetail",
},
{
id: "goToDetail",
block: {
name: "event-click",
selector: ".product-title",
findBy: "cssSelector",
option: { waitForSelector: true },
},
delayAfterMs: 500,
next: "readDetail",
},
{
id: "handleSoldOut",
block: {
name: "event-click",
selector: ".notify-me",
findBy: "cssSelector",
option: { waitForSelector: true },
},
next: "readDetail",
},
{
id: "readDetail",
block: {
name: "get-text",
selector: ".product-detail",
findBy: "cssSelector",
useTextContent: true,
option: { waitForSelector: true },
},
},
],
};
const result = await client.collectWorkflow({
targetUrl: location.href,
workflow,
closeTabAfterCollection: true,
});
console.log(result.steps.readDetail.result.data);checkExtension(): ๋ธ๋ผ์ฐ์ ํ์ฅ ํ๋ก๊ทธ๋จ ์ฐ๊ฒฐ ์ํ ํ์ธcollectData({ targetUrl, block, blockDelay }): ๋จ์ผ ๋ธ๋ก ๋๋ ๋ธ๋ก ๋ฐฐ์ด ์คํcollectWorkflow({ targetUrl, workflow, closeTabAfterCollection, activateTab }): ์ํฌํ๋ก์ฐ ์คํ
์ํฌํ๋ก์ฐ JSON ๊ตฌ์กฐ ๋ฐ ๊ณ ๊ธ ์ฌ์ฉ๋ฒ์
WORKFLOW_EXECUTION_ARCHITECTURE.md๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.