0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Genieeさんでのインターン体験記:カレンダーアプリ改造

Posted at

はじめに

今回、初めて開発系のインターンに参加させていただいたので記録を残してみます。普段の専門はハードウェアセキュリティで、セキュリティやインフラ系のインターンばかりでWebアプリの開発を専門としていないので結構緊張していました。一応、長期インターンでWebアプリケーションを作ったりすることもあるのでそこに自分の専門となるセキュリティの目線から今回のインターンに取り組んでみました。今回のインターンは1dayで昼から夕方の間、各自オフィスの好きな場所でひたすら提示された課題の解決と追加機能の実装を行う形式でした。2つの日程のうち1日目に参加しましたが、参加者の上位6位まで副賞があり今回2位をいただけました。

Geineeさんについて

広告プラットフォーム事業
メディア(Webサイト運営者など)の広告収益を最大化するプラットフォーム(SSPや営業支援ツール(SFA)や顧客管理システム(CRM)を提供している会社さんです。
ドラマのロケ地に使われることの多いオフィスというだけあり、とても綺麗でしたオフィス内にポーカー、麻雀、卓球台...etcと数々の他の会社ではみないような物も設置されていて感動しました。
福利厚生がとてもしっかりしていて社員のAI導入や書籍購入補助などに積極的なのがかなり魅力的でしたった。

VlZxR-HaSwmxFe_gyGnpRg.jpgd8g9Y3akT-Suxi5gU3Jchw.jpg
uploading...0

インターンの課題概要:既存カレンダーアプリの改善

与えられたプロジェクト概要

改善対象は、React + TypeScript(フロントエンド)とFastAPI + Python(バックエンド)で構成されたカレンダーWebアプリケーションでした。

課題構成

  • Phase 1: フロントエンド・バックエンド各6問の演習問題
  • Phase 2: フロント・バック連携の統合演習
  • Phase 3: 独自の改善・最適化実装

フロントエンド課題(6項目)と解決方法

  1. 分類ラベルの追加: 定数配列にcustomer_visits、internal_meetings、external_meetingsを追加し、プルダウンメニューのoption要素を拡張
  2. ヘッダーのUI調整: CSSのFlexboxを活用し、justify-content: space-betweenで左右配置、justify-content: centerで中央配置を実装
  3. レスポンシブ対応: CSS Media Queryで@media (max-width: 600px)を設定し、カレンダーにoverflow-x: scrollとmin-widthを指定
  4. 予定追加のUI/UX改善: input要素にrequired属性追加、useState hookでタイトル値を監視し、空欄時はbutton要素のdisabled属性をtrueに設定
  5. 予定表示のバグ修正: dayjs.format('YYYY/MM/DD HH:mm')を使用し、詳細表示と入力フォームで同一の日時フォーマット関数を共通化
  6. 予定削除機能の追加: Material-UIのDeleteIconを詳細ダイアログに配置し、onClick時にfetchAPIでDELETEリクエストを送信する処理を実装

バックエンド課題(6項目)と解決方法

  1. ジャンルフィルタ機能実装: FastAPIのQuery parametersを使用してgenre: Optional[str]を追加し、SQLAlchemyのfilter(Event.genre == genre)でクエリ実行
  2. ユーザータイムスタンプ機能実装: SQLAlchemyのColumn定義にDateTime(default=datetime.utcnow)とDateTime(onupdate=datetime.utcnow)を追加
  3. APIエラーメッセージ統一: HTTPExceptionのdetailパラメータを指定通りの英語メッセージに変更し、認証エラー時は統一的に"Not authenticated"を返すよう修正
  4. イベント重複チェック機能実装: SQLAlchemyクエリでstart_datetime < end_datetimeとend_datetime > start_datetimeの条件で時間重複を検出する関数を新規作成
  5. イベント継続時間計算機能実装: Python datetimeのtimedelta.total_seconds()を活用し、分単位変換後にint型でAPIレスポンスのduration_minutesフィールドに追加
  6. APIヘルスチェック機能実装: app/routes/health.pyを新規作成し、データベース接続テスト後にstatusとtimestampを含むJSONレスポンスを返すエンドポイントを実装

元のプログラム(your-name)の詳細分析

技術構成の詳細調査

実装を調査した結果、以下の技術スタックが使用されていました:

フロントエンド技術詳細

// 主要ライブラリ構成
- React 18 + TypeScript
- Material-UI (@mui/material, @mui/x-date-pickers)
- FullCalendar (@fullcalendar/react) - カレンダー表- dayjs - 日時操作ライブラリtimezone, utcプラグイン含む
- Vite - 高速ビルドツール
- Vitest + @testing-library - テスト環境

バックエンド技術詳細

# 主要フレームワーク・ライブラリ
- FastAPI - モダンなPython WebAPI フレームワーク
- SQLAlchemy - Python ORM
- SQLite - 軽量データベース
- Pydantic - データバリデーション
- Uvicorn - ASGI サーバー
- Pytest - テストフレームワーク

アプリケーション構造の分析

ディレクトリ構造

your-name/
├── frontend/
│   ├── src/
│   │   ├── features/          # 機能別コンポーネント
│   │   │   ├── api.ts        # API通信処理
│   │   │   └── routes/       # ルーティング
│   │   ├── components/       # 共通コンポーネント
│   │   │   └── FullCalendar.tsx
│   │   └── utils/           # ユーティリティ
│   └── mock-server/         # 開発用モックAPI
└── backend/
    ├── app/
    │   ├── models/          # SQLAlchemyモデル
    │   ├── routes/          # APIルート定義
    │   ├── schemas/         # Pydanticスキーマ
    │   └── utils/           # ユーティリティ
    └── tests/               # テストファイル

発見したセキュリティ上の改善点

コードレビューを進める中で、セキュリティ上の改善が必要な箇所を発見しました。

問題1: 認証ロジックの改善点

your-name/backend/app/routes/auth.py:19行目

def verify_credentials(credentials: HTTPBasicCredentials, db: Session):
    user = db.query(User).filter(User.username == credentials.username).first()
    
    # 脆弱性
    if user and credentials.username == credentials.password:
        return credentials.username
    
    raise HTTPException(status_code=401, detail="Authentication required")

この実装の改善点

  1. パスワード検証の簡略化: データベースのパスワードハッシュを参照せず、ユーザー名とパスワードが同じ場合のみ認証成功
  2. 既存セキュリティ機能の未活用: security.pyで実装されたハッシュ化機能が活用されていない
  3. より堅牢な認証への改善余地: パスワードハッシュ検証による認証強化が可能

問題2: フロントエンド認証情報の扱い

your-name/frontend/src/features/api.ts:22-24行目

const credentials = btoa(
  `${import.meta.env.VITE_USERNAME}:${import.meta.env.VITE_PASSWORD}`
);

環境変数からの認証情報取得は適切でしたが、バックエンドの認証ロジック簡略化により、より堅牢な認証システムへの改善余地がありました。

改善実装の詳細プロセス

Phase 1: セキュリティ改善の実装

認証ロジックの強化

最初に取り組んだのは、認証機能の完全な再実装でした:

# aoba-tonosaki/backend/app/routes/auth.py:19行目(改善版)
def verify_credentials(credentials: HTTPBasicCredentials, db: Session):
    user = db.query(User).filter(User.username == credentials.username).first()
    
    # セキュアな認証実装
    if user and verify_password(credentials.password, user.password_hash):
        return credentials.username
    
    # エラーメッセージも統一
    raise HTTPException(
        status_code=401, 
        detail="Not authenticated",
        headers={"WWW-Authenticate": "Basic"}
    )

改善ポイント

  • verify_password()関数による適切なハッシュ検証
  • ソルト付きSHA256ハッシュとの安全な比較
  • secrets.compare_digest()によるタイミング攻撃対策

セキュリティ強化の実装詳細

# security.py の活用
def verify_password(plain_password: str, hashed_password: str) -> bool:
    try:
        # ソルトとハッシュを分離
        salt, stored_hash = hashed_password.split("$")
        # 同じソルトでハッシュ化
        password_hash = hashlib.sha256(f"{plain_password}{salt}".encode()).hexdigest()
        # タイミング攻撃対策付き比較
        return secrets.compare_digest(password_hash, stored_hash)
    except ValueError:
        # 後方互換性維持
        return secrets.compare_digest(plain_password, hashed_password)

Phase 2: 課題要件の完全実装

バックエンド課題(6項目)の技術実装

1. ジャンルフィルタ機能

# SQLAlchemyクエリの実装
@router.get("/", response_model=List[EventResponse])
def get_events(genre: Optional[str] = None, db: Session = Depends(get_db)):
    query = db.query(Event)
    if genre:
        query = query.filter(Event.genre == genre)
    return query.all()

2. ユーザータイムスタンプ機能

# SQLAlchemyモデル拡張
class User(Base):
    # ... existing fields
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

3. イベント重複チェック機能

# utils/validation.py
def check_event_overlap(start_datetime: datetime, end_datetime: datetime, 
                       user_id: int, db: Session, exclude_event_id: int = None) -> bool:
    query = db.query(Event).filter(
        Event.user_id == user_id,
        Event.start_datetime < end_datetime,
        Event.end_datetime > start_datetime
    )
    if exclude_event_id:
        query = query.filter(Event.id != exclude_event_id)
    return query.first() is not None

フロントエンド課題(6項目)の実装

1. レスポンシブ対応

// Material-UIテーマ活用
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));

return (
  <Grid container spacing={isMobile ? 1 : 2}>
    <Grid item xs={12} md={8}>
      <FullCalendar
        height={isMobile ? 400 : 600}
        // ... その他の設定
      />
    </Grid>
  </Grid>
);

2. UI/UX改善

// Material-UI コンポーネント活用
<Button
  variant="contained"
  color="primary"
  startIcon={<AddIcon />}
  onClick={handleEventAdd}
  sx={{
    borderRadius: 2,
    textTransform: 'none',
    fontWeight: 600
  }}
>
  新しい予定を追加
</Button>

Phase 3: 独自機能の実装

1. Work Analytics機能(主要追加機能)

個人的な体験から生まれた機能です。いつも「今月の給与いくらぐらいになるかな」って手計算しているのが面倒だったので、こういうカレンダー機能があればいいなと思って追加しました。実際の業務で使えるレベルを目指し、以下の機能を実装:

実装内容

  • 時給制、日給制、月給制の3つの支払いタイプに対応
  • 月給制では労働基準法に基づく残業代自動計算(基本時間160時間、残業倍率1.25倍)
  • Chart.jsを使用したグラフ表示による月次労働状況の可視化
  • 目標時間設定と実績の進捗表示機能
// Work Analytics コンポーネント
interface WorkAnalyticsProps {
  paymentType: 'hourly' | 'daily' | 'monthly';
  baseRate: number;
  overtimeRate?: number;
  targetHours?: number;
}

const WorkAnalytics: React.FC<WorkAnalyticsProps> = ({
  paymentType, baseRate, overtimeRate = 1.25, targetHours = 160
}) => {
  const [monthlyData, setMonthlyData] = useState<WorkData[]>([]);
  
  // 月給制の残業代計算
  const calculateMonthlySalary = (workedHours: number) => {
    const standardHours = targetHours;
    if (workedHours <= standardHours) {
      return baseRate;
    }
    
    const overtimeHours = workedHours - standardHours;
    const hourlyRate = baseRate / standardHours;
    const overtimePay = overtimeHours * hourlyRate * overtimeRate;
    
    return baseRate + overtimePay;
  };

  // Chart.js による可視化
  return (
    <Card>
      <CardContent>
        <Typography variant="h6">Work Analytics</Typography>
        <Bar
          data={chartData}
          options={{
            responsive: true,
            plugins: {
              legend: { position: 'top' },
              title: { display: true, text: '月次労働状況' }
            }
          }}
        />
      </CardContent>
    </Card>
  );
};

2. 秘密スケジュール機能

写真の秘密のフォルダ感覚で、万が一人に見られてもいい予定を追加できる機能です。個人的なプライベートな予定や、他の人には見られたくない重要な予定を安全に管理できるよう実装しました。

実装内容

  • 予定作成時に「秘密の予定」としてマークする機能
  • SHA-256ハッシュ化によるパスワード保護
  • カレンダー表示では「秘密の予定」として表示し、クリック時にパスワード入力ダイアログを表示
  • 正しいパスワード入力後に詳細内容を表示する仕組み
// パスワード保護されたイベント表示
const SecretEventModal: React.FC<{ event: Event }> = ({ event }) => {
  const [password, setPassword] = useState('');
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const verifyPassword = async (inputPassword: string) => {
    const hashedInput = await crypto.subtle.digest(
      'SHA-256',
      new TextEncoder().encode(inputPassword + event.salt)
    );
    
    return Array.from(new Uint8Array(hashedInput))
      .map(b => b.toString(16).padStart(2, '0'))
      .join('') === event.passwordHash;
  };

  if (!isAuthenticated) {
    return (
      <Dialog open={true}>
        <DialogContent>
          <TextField
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            placeholder="パスワードを入力"
          />
        </DialogContent>
      </Dialog>
    );
  }

  return <EventDetailView event={event} />;
};

3. Google Calendar CSV連携機能

他のカレンダーからの移行をスムーズにするのと、万が一クラウドが壊れてもユーザー側がデータを保管できるという安心感を提供したくて実装した機能です。特にデータの可搬性とバックアップという観点で重要だと考えました。

実装内容

  • Google Calendar互換のCSV形式でのインポート・エクスポート機能
  • CSV読み込み時の自動ジャンル推定(タイトルから会議、作業などを判定)
  • マルチフォーマット日時解析(MM/DD/YYYY形式対応)
  • ワンクリックでのデータエクスポート機能

4. イベントテンプレート機能

繰り返し行われる用事をテンプレート化できる機能を実装しました。定期的に発生する会議や作業をワンクリックで作成できるようにし、効率的なスケジュール管理を実現しました。

実装内容

  • よく使用するイベントをテンプレートとして保存
  • テンプレート名、デフォルトの開始・終了時間、ジャンル、説明文の設定
  • テンプレート一覧からワンクリックでイベント作成
  • テンプレートの編集・削除機能

5. 表示モード切り替え機能

プライベートだけ表示するモードと全表示モードを切り替えて、仕事関係の予定を表示するかを選択できる機能を実装しました。用途に応じてカレンダーの表示内容を絞り込むことで、集中したいコンテキストに応じた使い分けが可能です。

実装内容

  • プライベートモード:private、otherジャンルのみ表示
  • 全表示モード:すべてのジャンルを表示
  • ヘッダーでの表示モード切り替えボタン実装
// CSV import/export 機能
const GoogleCalendarIntegration = () => {
  const exportToCSV = (events: Event[]) => {
    const csvContent = [
      ['Subject', 'Start Date', 'Start Time', 'End Date', 'End Time', 'Description'],
      ...events.map(event => [
        event.title,
        dayjs(event.start_datetime).format('MM/DD/YYYY'),
        dayjs(event.start_datetime).format('HH:mm:ss'),
        dayjs(event.end_datetime).format('MM/DD/YYYY'),
        dayjs(event.end_datetime).format('HH:mm:ss'),
        event.description || ''
      ])
    ].map(row => row.join(',')).join('\n');

    const blob = new Blob([csvContent], { type: 'text/csv' });
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `calendar_export_${dayjs().format('YYYY-MM-DD')}.csv`;
    a.click();
  };

  const importFromCSV = (file: File) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const csv = e.target?.result as string;
      const lines = csv.split('\n');
      const events = lines.slice(1).map(line => {
        const [subject, startDate, startTime, endDate, endTime, description] = line.split(',');
        return {
          title: subject,
          start_datetime: dayjs(`${startDate} ${startTime}`, 'MM/DD/YYYY HH:mm:ss').toISOString(),
          end_datetime: dayjs(`${endDate} ${endTime}`, 'MM/DD/YYYY HH:mm:ss').toISOString(),
          description,
          genre: inferGenreFromTitle(subject)
        };
      });
      
      // バックエンドAPIに一括送信
      Promise.all(events.map(event => fetchApi('/api/events', {
        method: 'POST',
        body: JSON.stringify(event)
      })));
    };
    reader.readAsText(file);
  };

  return (
    <Card>
      <CardContent>
        <Typography variant="h6">External Integration</Typography>
        <Button component="label">
          CSV Import
          <input type="file" hidden accept=".csv" onChange={(e) => 
            e.target.files?.[0] && importFromCSV(e.target.files[0])
          } />
        </Button>
        <Button onClick={() => exportToCSV(events)}>
          CSV Export
        </Button>
      </CardContent>
    </Card>
  );
};

開発中の困難と解決過程

技術的な困難

1. TypeScript型安全性の確保

初めは型エラーが頻発し、Pydanticスキーマとの連携に苦労しました:

// 解決前:型定義が曖昧
interface Event {
  id?: number;
  title: string;
  // ... 他のフィールドが曖昧
}

// 解決後:バックエンドスキーマと完全同期
interface EventResponse {
  id: number;
  title: string;
  start_datetime: string; // ISO format
  end_datetime: string;
  genre: 'work' | 'personal' | 'meeting';
  all_day: boolean;
  description?: string;
  created_at: string;
  updated_at: string;
}

2. 非同期処理とエラーハンドリング

// React Query を導入してデータフェッチを改善
const { data: events, isLoading, error } = useQuery(
  ['events', genre, startDate, endDate],
  () => fetchApi<EventResponse[]>('/api/events', {
    method: 'GET',
    // ... parameters
  }),
  {
    retry: 3,
    staleTime: 5 * 60 * 1000, // 5分間キャッシュ
    onError: (error) => {
      toast.error(`イベントの取得に失敗しました: ${error.message}`);
    }
  }
);

テスト・品質保証

バックエンドテスト(Pytest)

def test_secure_authentication():
    """改善された認証機能のテスト"""
    # パスワードハッシュ検証
    response = client.post("/api/auth/login", 
        auth=("testuser", "testuser"))
    assert response.status_code == 200
    
    # 間違ったパスワード
    response = client.post("/api/auth/login", 
        auth=("testuser", "wrongpassword"))
    assert response.status_code == 401
    assert response.json()["detail"] == "Not authenticated"

def test_work_analytics_calculation():
    """Work Analytics機能のテスト"""
    # 月給制残業代計算
    result = calculate_monthly_salary(
        worked_hours=180, 
        base_salary=300000, 
        standard_hours=160
    )
    expected = 300000 + (20 * (300000/160) * 1.25)  # 基本給 + 残業代
    assert result == expected

フロントエンドテスト(Vitest + Testing Library)

test('Work Analytics component renders correctly', () => {
  render(<WorkAnalytics paymentType="monthly" baseRate={300000} />);
  
  expect(screen.getByText('Work Analytics')).toBeInTheDocument();
  expect(screen.getByText('月次労働状況')).toBeInTheDocument();
});

test('CSV export functionality', () => {
  const events: EventResponse[] = [
    {
      id: 1,
      title: 'テスト会議',
      start_datetime: '2025-01-15T10:00:00Z',
      end_datetime: '2025-01-15T11:00:00Z',
      genre: 'meeting',
      all_day: false
    }
  ];
  
  const csvContent = exportToCSV(events);
  expect(csvContent).toContain('テスト会議');
  expect(csvContent).toContain('01/15/2025');
});

最終成果物

実装機能

  • 全Phase1課題(フロント6項目 + バック6項目)
  • Phase2統合演習
  • Work Analytics機能(3つの支払いタイプ対応)
  • 秘密スケジュール機能
  • Google Calendar CSV連携
  • レスポンシブデザイン
  • 全21項目のテスト成功

セキュリティ改善

  • 認証ロジックの改善(パスワードハッシュ検証の実装)
  • パスワードハッシュ化の適切な活用
  • エラーメッセージ統一による一貫性向上
  • 入力値検証・SQLインジェクション対策

まとめ

セキュリティかインフラの極端な例でばかり色々な会社さんのインターンに参加をしてきたので今回のインターンはとても新鮮な体験でした。実際の開発現場でのAIの活用が進んでいることも実感し、AIの利用についてももっと勉強しないといけないのかなと感じました。また、いつもセキュリティ系のインターンに行くと知り合いだらけの中今回全員初めましての人しかいなくて、他の学生さんとの会話も他分野の話を沢山聞くことが出来てとても楽しい体験となりました。普段、1からアプリ開発をすることはあっても既存のアプリケーションへの機能補足や実装不備部分を見つけ出して完成させるといった体験は中々なかったのでアプリ開発の面でも貴重な体験を得ることができました。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?