Skip to content
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
100 changes: 100 additions & 0 deletions src/components/SlackMembershipModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Alert, Button, FormField, Input, Modal } from "../design-system";
import { useAuth } from "../hooks/useAuth";
import { authService } from "../services/authService";

const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const DISMISSED_KEY = "slackMembershipModalDismissed";

const wasDismissed = () => {
try {
return sessionStorage.getItem(DISMISSED_KEY) === "1";
} catch {
return false;
}
};

const rememberDismissed = () => {
try {
sessionStorage.setItem(DISMISSED_KEY, "1");
} catch {
// 저장하지 못하면 이번 화면에서만 닫힌다.
}
};

/**
* Slack에서 이 사람을 찾지 못했을 때만 뜬다. 지금 신청하면 거절되므로, 신청 전에 Slack에 가입한 이메일을 받아 둔다.
* 닫으면 이번 접속 동안은 다시 뜨지 않고, 확인되면 그 뒤로는 뜨지 않는다.
*/
export default function SlackMembershipModal() {
const { t } = useTranslation();
const { user, updateUser } = useAuth();
const [visible, setVisible] = useState(false);
const [contactEmail, setContactEmail] = useState("");
const [error, setError] = useState("");
const [saving, setSaving] = useState(false);

useEffect(() => {
if (!user || wasDismissed()) return undefined;
let cancelled = false;
authService.getSlackMembership()
.then((status) => {
if (cancelled || status !== "NOT_MEMBER") return;
setContactEmail(user.contactEmail || "");
setVisible(true);
})
.catch(() => {
// 확인하지 못하면 띄우지 않는다. 신청할 때 다시 확인한다.
});
return () => { cancelled = true; };
// 접속한 사람이 바뀔 때만 다시 확인한다.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.userId]);

const dismiss = () => {
rememberDismissed();
setVisible(false);
};

const save = async () => {
const value = contactEmail.trim();
if (!value || !EMAIL_PATTERN.test(value)) {
setError(t("auth.contactEmailInvalid"));
return;
}
setSaving(true);
setError("");
try {
await authService.updateContactEmail(value);
await updateUser();
if ((await authService.getSlackMembership()) === "NOT_MEMBER") {
setError(t("slackModal.stillNotFound"));
} else {
setVisible(false);
}
} catch (e) {
// 서버가 답한 오류는 원인별 문구를 그대로 쓰고, 연결 실패 등은 기본 문구를 쓴다.
setError(e?.status && e.message ? e.message : t("account.profileSaveFailed"));
} finally {
setSaving(false);
}
};

return (
<Modal
visible={visible}
onDismiss={dismiss}
header={t("slackModal.title")}
footer={<><Button onClick={dismiss}>{t("slackModal.later")}</Button><Button variant="primary" loading={saving} disabled={saving} onClick={save}>{t("common.save")}</Button></>}
>
<div className="space-y-4">
<p>{t("slackModal.body")}</p>
<FormField label={t("auth.contactEmail")} errorText={error} constraintText={t("slackModal.emailHelp", { email: user?.email || "" })} htmlFor="slack-modal-contact-email">
<Input id="slack-modal-contact-email" type="email" value={contactEmail} onChange={(value) => { setContactEmail(value); setError(""); }} invalid={!!error} />
</FormField>
<Alert type="info">{t("slackModal.nameNote", { name: user?.name || "" })}</Alert>
</div>
</Modal>
);
}
21 changes: 17 additions & 4 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"department": "Department",
"studentId": "Student ID",
"phone": "Phone",
"contactEmail": "Frequently used email",
"contactEmailHelp": "Enter the email you joined Slack with. If you joined with your school email, keep it the same or leave it blank.",
"contactEmailInvalid": "The email format is not valid.",
"verificationCode": "Verification code",
"login": "Log in",
"signup": "Sign up",
Expand Down Expand Up @@ -61,7 +64,7 @@
"previous": "Previous",
"ubuntuUsername": "Ubuntu username",
"domainHelp": "Use a dgu.ac.kr or dongguk.edu email address.",
"slackHelp": "Use the exact same name as your Slack display name to receive notifications.",
"slackHelp": "Use the exact same name as your Slack display name. Otherwise you cannot request a server or receive notifications.",
"ubuntuUsernameHelp": "The account name you use to SSH into your containers. Every container you get uses this name, and it cannot be changed after signup."
},
"shell": {
Expand Down Expand Up @@ -154,6 +157,8 @@
},
"wizard": {
"title": "GPU request",
"slackTitle": "Join Slack before requesting",
"slackBody": "You must be a member of the Slack workspace to submit a request. Your Slack name must match the name in your profile, and the email you joined Slack with must match your school email or the 'Frequently used email' in account settings.",
"roleLeader": "Team leader",
"roleMember": "Team member",
"teamStep1": "Create a group with 'Create a new shared group' below, then submit this request.",
Expand Down Expand Up @@ -414,12 +419,20 @@
"deniedOn": "Denied: {{date}}",
"adminComment": "Administrator comment"
},
"slackModal": {
"title": "We could not find you on Slack",
"body": "You must be a member of the Slack workspace to request a server. Enter the email you joined Slack with.",
"emailHelp": "If you joined Slack with an address other than your school email ({{email}}), enter that address.",
"nameNote": "Your Slack name (display name or real name) must also exactly match the name in your profile, \"{{name}}\". If you have not joined Slack yet, join first.",
"stillNotFound": "Saved, but we still could not find you on Slack. Check the email and your Slack name again.",
"later": "Later"
},
"account": {
"phoneRequired": "Enter your phone number.",
"phoneSaved": "Your phone number was updated.",
"phoneSaveFailed": "Your phone number could not be updated. Please try again shortly.",
"profileSaved": "Saved.",
"profileSaveFailed": "Could not save. Please try again shortly.",
"basicTitle": "Basic information",
"basicDesc": "Only the phone number can be changed.",
"basicDesc": "Only the phone number and frequently used email can be changed.",
"emailMissing": "No email",
"studentIdMissing": "No student ID",
"nameMissing": "No name",
Expand Down
21 changes: 17 additions & 4 deletions src/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"department": "학과",
"studentId": "학번",
"phone": "전화번호",
"contactEmail": "자주 사용하는 이메일",
"contactEmailHelp": "Slack에 가입한 이메일을 적어 주세요. 학교 이메일로 가입했다면 학교 이메일과 같게 적거나 비워 두면 돼요.",
"contactEmailInvalid": "이메일 형식이 올바르지 않아요.",
"verificationCode": "인증번호",
"login": "로그인",
"signup": "회원가입",
Expand Down Expand Up @@ -61,7 +64,7 @@
"previous": "이전",
"ubuntuUsername": "Ubuntu 사용자명",
"domainHelp": "dgu.ac.kr 또는 dongguk.edu 이메일만 사용할 수 있습니다.",
"slackHelp": "Slack 표시 이름과 동일하게 입력해야 알림을 받을 수 있습니다.",
"slackHelp": "Slack 표시 이름과 똑같이 적어 주세요. 다르면 서버를 신청할 수 없고 알림도 받지 못해요.",
"ubuntuUsernameHelp": "컨테이너에 SSH로 접속할 때 쓰는 계정 이름입니다. 앞으로 받는 모든 컨테이너에 같은 이름이 쓰이며, 가입 후에는 변경할 수 없습니다."
},
"shell": {
Expand Down Expand Up @@ -151,6 +154,8 @@
},
"wizard": {
"title": "GPU 신청",
"slackTitle": "신청 전에 Slack 가입이 필요해요",
"slackBody": "Slack 워크스페이스에 가입되어 있어야 신청할 수 있어요. Slack 이름은 회원 정보의 이름과 같아야 하고, Slack에 가입한 이메일은 학교 이메일이나 계정 설정의 '자주 사용하는 이메일'과 같아야 해요.",
"roleLeader": "팀장",
"roleMember": "팀원",
"teamStep1": "아래 '새 공유 그룹 만들기'로 그룹을 만들고, 이 신청서를 끝까지 제출해요.",
Expand Down Expand Up @@ -410,12 +415,20 @@
"deniedOn": "거절: {{date}}",
"adminComment": "관리자 의견"
},
"slackModal": {
"title": "Slack에서 회원님을 찾지 못했어요",
"body": "서버를 신청하려면 Slack 워크스페이스에 가입되어 있어야 해요. Slack에 가입할 때 쓴 이메일을 적어 주세요.",
"emailHelp": "학교 이메일({{email}})이 아닌 주소로 Slack에 가입했다면 그 주소를 적어 주세요.",
"nameNote": "Slack 이름(표시 이름 또는 실명)도 회원 정보의 이름 \"{{name}}\"과 똑같아야 해요. 아직 Slack에 가입하지 않았다면 먼저 가입해 주세요.",
"stillNotFound": "저장했지만 아직 Slack에서 찾지 못했어요. 이메일과 Slack 이름을 다시 확인해 주세요.",
"later": "나중에"
},
"account": {
"phoneRequired": "전화번호를 입력해 주세요.",
"phoneSaved": "전화번호를 바꿨어요.",
"phoneSaveFailed": "전화번호를 바꾸지 못했어요. 잠시 후 다시 시도해 주세요.",
"profileSaved": "저장했어요.",
"profileSaveFailed": "저장하지 못했어요. 잠시 후 다시 시도해 주세요.",
"basicTitle": "기본 정보",
"basicDesc": "휴대폰 번호만 바꿀 수 있어요.",
"basicDesc": "휴대폰 번호와 자주 사용하는 이메일만 바꿀 수 있어요.",
"emailMissing": "이메일 정보 없음",
"studentIdMissing": "학번 정보 없음",
"nameMissing": "이름 정보 없음",
Expand Down
35 changes: 32 additions & 3 deletions src/pages/AccountPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@
</Alert>
) : null;

const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const AccountPage = ({ user }) => {
const { t, i18n } = useTranslation();
const { updateUser } = useAuth();
const [formData, setFormData] = useState({
phone: "",
contactEmail: "",
});
const [errors, setErrors] = useState({});
const [profileLoading, setProfileLoading] = useState(false);
Expand All @@ -41,6 +44,7 @@
if (user) {
setFormData({
phone: user.phone || "",
contactEmail: user.contactEmail || "",
});
}
}, [user]);
Expand Down Expand Up @@ -69,6 +73,10 @@
newErrors.phone = phoneFormatError();
}

if (formData.contactEmail.trim() && !EMAIL_PATTERN.test(formData.contactEmail.trim())) {
newErrors.contactEmail = t("auth.contactEmailInvalid");
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
Expand All @@ -84,13 +92,18 @@
setProfileAlert(null);

try {
await authService.updatePhone(formData.phone);
setProfileAlert({ type: "success", message: t("account.phoneSaved") });
if (formData.phone !== (user?.phone || "")) {
await authService.updatePhone(formData.phone);
}
if (formData.contactEmail.trim() !== (user?.contactEmail || "")) {
await authService.updateContactEmail(formData.contactEmail.trim());
}
setProfileAlert({ type: "success", message: t("account.profileSaved") });
await updateUser();
} catch (error) {
setProfileAlert({
type: "error",
message: serverMessageOr(error, t("account.phoneSaveFailed")),
message: serverMessageOr(error, t("account.profileSaveFailed")),
});
} finally {
setProfileLoading(false);
Expand All @@ -98,7 +111,7 @@
};

const profileTabContent = (
<div className="space-y-6 mx-auto max-w-[900px]">

Check warning on line 114 in src/pages/AccountPage.jsx

View workflow job for this annotation

GitHub Actions / build

Raw px value — use a DECS spacing token
<Header
variant="h3"
description={t("account.basicDesc")}
Expand Down Expand Up @@ -146,6 +159,21 @@
placeholder="010-1234-5678"
/>
</FormField>
<FormField
label={t("auth.contactEmail")}
errorText={errors.contactEmail}
constraintText={t("auth.contactEmailHelp")}
htmlFor="account-contact-email"
>
<Input
id="account-contact-email"
type="email"
value={formData.contactEmail}
onChange={(value) => handleProfileChange({ target: { name: "contactEmail", value } })}
invalid={!!errors.contactEmail}
placeholder={user?.email || ""}
/>
</FormField>
</div>

<ResultAlert alert={profileAlert} onDismiss={() => setProfileAlert(null)} />
Expand All @@ -157,6 +185,7 @@
e.preventDefault();
setFormData({
phone: user?.phone || "",
contactEmail: user?.contactEmail || "",
});
setErrors({});
}}
Expand All @@ -172,7 +201,7 @@
);

return (
<div className="space-y-6 mx-auto max-w-[900px]">

Check warning on line 204 in src/pages/AccountPage.jsx

View workflow job for this annotation

GitHub Actions / build

Raw px value — use a DECS spacing token
{/* Header */}
<Header
variant="h1"
Expand Down
Loading
Loading